May 13, 2019·6 min read·engineeringrustsecurityerrorssoundness

Rust withdraws Error type identity overrides to preserve safe downcasts

CVE-2019-12083 lets a manual Error::type_id implementation mislead safe downcasting code. The announced mitigation withdraws a recently stabilized method whose result the library trusted for an unsafe cast.

A safe library method cannot use an overridable claim about a value's type as its only justification for an unsafe cast. Rust's newly stabilized Error::type_id method exposed exactly that problem. An implementation could return the identity of another type, and the standard library's downcast operations would trust that answer when interpreting the value's memory.

The May 13 security advisory identifies the vulnerability as CVE-2019-12083. It affects manual implementations of Error::type_id interacting with the Error::downcast family. The immediate mitigation is to remove manual implementations and inherit the standard library's correct default implementation.

The team announces a Rust 1.34.2 release for tomorrow to make the method unstable again. As of this advisory, that is a planned release. The team also plans to update beta, nightly and the forthcoming 1.35 release, while stating that the final API design remains undecided.

I find Rust's safety contract especially valuable when ordinary application code can use a library without auditing its pointer casts. This defect matters because the public interface allowed safe code to invalidate the assumption behind those casts. Correcting that interface is necessary even when it disrupts a feature stabilized only recently.

The default implementation answers the right question

The Rust 1.34 source defines the default type_id implementation using TypeId::of::<Self>(). Under the method's lifetime requirement, that obtains the identity of the implementing type. Downcasting needs precisely that identity to determine whether the hidden concrete value matches the requested target type.

The difficulty is that type_id is a trait method with an overridable default. A downstream implementation can supply a different body. The compiler can check the returned value's Rust type, TypeId, without establishing that it identifies the implementation's actual concrete type.

Those are different properties. Returning a valid TypeId object meets the method signature. Returning the identity of the actual underlying value meets the invariant needed by the downcast. The interface allowed a safe implementation to satisfy the first while violating the second.

The advisory demonstrates the issue with an implementation returning another type's identity. That example is evidence of the incorrect trust boundary, rather than a recommended way to customize conversion behavior. Type identity is not a request to reinterpret the value as any type the implementation chooses.

Downcasting turns the answer into memory access

The source shows is<T> comparing the target's TypeId with the result returned by self.type_id(). A matching answer then allows downcast_ref<T> to cast the trait object's data pointer to a pointer to T and return a reference. downcast_mut<T> follows the corresponding mutable route.

A boxed downcast similarly checks the reported identity before converting the raw boxed pointer into a box of the requested type. These functions expose safe interfaces, with unsafe operations inside their implementation. Their safety depends on the identity comparison establishing that the memory really contains the requested type.

If a manual method lies about that identity, the comparison no longer establishes the necessary fact. The unsafe cast can then create access to a value through the wrong type. The advisory names out-of-bounds reads and writes among the possible security consequences.

The failure is not merely that an application receives an incorrect error classification. Classification authorizes pointer interpretation, and that interpretation has memory-safety requirements. A type check returning the wrong boolean can therefore have consequences beyond ordinary control-flow mistakes.

This is also why the existence of unsafe code inside the standard library is not itself the defect. Safe interfaces routinely use carefully justified unsafe operations internally. The broken part is the justification, which depended on a result safe downstream code could override incorrectly.

Stable exposure changed the practical boundary

The advisory dates stabilization of Error::type_id to Rust 1.34.0 on April 11. Rust 1.34.1, released April 25, is also affected. The 1.34 release announcement lists the method among its library stabilizations.

Before that stabilization, stable code could not call or implement the method. The advisory says the unstable method had existed since Rust 1.0.0, so nightly code may have been affected over a much longer interval. Stable exposure and historical implementation presence must therefore be described separately.

The newly stable interface changed what ordinary crates could express without unstable features. That is the specific expansion being reversed. It does not mean all uses of trait-object errors became unsafe in 1.34, or that every downcast in an affected toolchain necessarily misinterprets memory.

The team states that code without a manual Error::type_id implementation is not affected. That is a useful scope condition. Normal error implementations inheriting the default retain the intended relationship between concrete type and reported identity.

I prefer this concrete condition to treating all error handling as suspect. An investigation can identify the manual implementations and examine whether downcasting relies on them. The advisory supplies a specific behavior to remove, rather than asking every application to replace its error model.

The announced fix accepts a compatibility break

Making Error::type_id unstable again will break stable code that calls it as well as code that overrides it. The advisory acknowledges both groups. It argues that the stable exposure has lasted only a few weeks, limiting the expected disruption, while manual overrides are likely to be memory unsafe.

The mitigation does not redesign every downcast operation in the announcement. It removes the newly stable customization point from ordinary stable code. The final fate of the method remains under discussion, and the advisory explicitly says nightly code may continue to exhibit the issue because no further action is currently planned beyond making it unstable.

That qualification matters for upgrade advice. A forthcoming stable point release is a particular response to stable exposure. It should not be described as a completed solution for every nightly use of the underlying API. Removing manual implementations is the immediate code-level mitigation stated today.

The source also makes clear why changing only a comment would be insufficient. An undocumented invariant cannot serve as a memory-safety obligation imposed on arbitrary safe trait implementations. The public interface must prevent safe callers and implementers from producing the state that makes its internal cast invalid.

Calls and overrides also have different migration implications. A caller may only be inspecting the identity returned by the correct default, without creating the malformed state itself. Its code still loses access to the stable method under the announced change. An override is the part that can provide the misleading answer consumed by downcasting, so removing it addresses the documented trigger directly.

Keeping those cases separate explains the advisory's compatibility discussion. Some code breaks because the public method disappears from stable use, while other code must stop overriding a result used as a safety check. Neither case requires assuming that every application error object already contains corrupt memory.

The disclosure identifies the implementation and response

The team's timeline records the report on May 9 and confirmation later that day. Mitigation work followed on May 10, with public pull requests and security-list notification on May 13. The advisory credits Sean McArthur for reporting the defect through the security process.

These dates establish what is known at publication. The planned May 14 point release is a next action, and the final design remains open. A contemporaneous account should retain those distinctions instead of describing later release outcomes as if they had already happened.

For library authors, the incident illustrates a precise audit question. When safe operations rely on a trait-provided value to justify unsafe memory access, can a safe implementation return a value that invalidates the justification? Here the answer was yes, because the reported identity could differ from the stored concrete type.

That question applies to the design of the abstraction, not to trusting the intentions of each implementer. Safe Rust code is allowed to be logically mistaken without causing undefined behavior through a sound safe interface. A library that needs a stronger invariant must establish it through a mechanism that safe customization cannot defeat.

The immediate response is therefore narrow and consequential. Inherit the correct default instead of manually implementing Error::type_id, and account for the announced withdrawal of the newly stable method. The downcast implementation needs reliable concrete type identity before returning a typed reference or box, and the overridable method failed to supply that reliability.