Rust 1.98 版本出现了一个严重的编译错误。
Rust 1.98 got a P-critical miscompilation

原始链接: https://github.com/rust-lang/rust/issues/161441

用户报告称在 Rust 1.98.0 版本中出现了一个回归问题,导致 `rama` 项目在 `aarch64-apple-darwin` 平台上发生 `SIGSEGV`(段错误)。 **问题描述:** Rust 1.98 为特定的装箱(boxed)服务生成了错误的虚函数表(vtable),导致偏移量 0x18 处的方法条目为零(空)。当代码通过该虚函数表进行调用分发时,会跳转到零地址,从而导致程序立即崩溃。此行为在 1.98.0 版本中稳定复现,但在 1.97.1 或后续的 nightly 构建版本中不会出现,这表明该问题可能与异步特质擦除(async trait erasure)或虚函数表生成相关的编译器漏洞有关。 **规避方案:** 作者已找到两种绕过错误虚函数表生成的方法: 1. **重构特定用法:** 将 `BoxService` 修改为使用 `Box` 包装器,通过移动泛型谓词边界来避免触发编译器的特定条件。 2. **全局规避方案:** 手动实现 `BoxService`,将分发函数与特质的虚函数表分离,利用 `Any` 进行类型擦除,并使用手动函数指针进行调用。 作者目前正在寻求确认这是否属于需要通过 1.98.1 版本修复的 `rustc` 漏洞,还是其实现模式在技术上存在不严谨之处。

抱歉。
相关文章

原文

View all comments

Observed on aarch64-apple-darwin.
It also consistently hits my CI runners since release of 1.98,
failing our CI... each commit (https://github.com/plabayo/rama)

The same Rama source works with Rust 1.97.1, crashes with
Rust 1.98.0, and works again with nightly-2026-07-16.

Rust 1.98 emits a zero method entry in a compiler-generated
vtable. Safe Rust dispatches through that entry and the process
segfaults at address zero.

Based on my CI failures I made a small example using our rama Repo,
note that it assumes TCP port 62017 is available:

git clone https://github.com/plabayo/rama.git
cd rama
git checkout --detach 0b4aa58e33e9d07db2718a3886c05b673ba9a70e

rustup toolchain install 1.98.0
just clean

CARGO_TARGET_DIR=target-ticket-198 \
cargo +1.98.0 build \
  -p rama-examples \
  --features=cli,tcp,http-full,proxy-full,boring \
  --bin http_mitm_proxy_boring

Than run:

./target-ticket-198/debug/http_mitm_proxy_boring \
  >/tmp/rama-server-198.log 2>&1 &
proxy_pid=$!

sleep 2

curl -skv --max-time 5 \
  --proxy http://john:[email protected]:62017 \
  https://127.0.0.1:1/ \
  >/tmp/rama-curl-198.log 2>&1 || true

if kill -0 "$proxy_pid" 2>/dev/null; then
  echo "proxy is still alive"
  kill "$proxy_pid"
  wait "$proxy_pid" 2>/dev/null || true
else
  wait "$proxy_pid"
  echo "proxy exited with status $?"
fi

sed -n '1,120p' /tmp/rama-curl-198.log
sed -n '1,120p' /tmp/rama-server-198.log

Repeat with separate targets (w/ clean target directory) for 1.97.1 and nightly-2026-07-16 (tried to narrow down...).

What you should see is the following:

  • 1.97.1: 502 Bad Gateway; proxy remains alive
  • 1.98.0: CONNECT aborts; SIGSEGV at address zero
  • nightly-2026-07-16: 502 Bad Gateway; proxy remains alive

In short, it is expected that the failed upstream connection should be converted into HTTP/1.1 502 Bad Gateway, and the proxy should remain alive.

Instead it is obsered in 1.98 that the CONNECT request is aborted. macOS reports SIGSEGV with pc=0. The generated caller loads the Service method from vtable + 0x18 and branches to the zero value stored there.

Diving into the code it seems that the Rust 1.98 vtable contains valid drop/size/alignment fields,
but the first method slot at offset 24 is zero. -Zprint-mono-items collects the erased caller on Rust 1.98, but
not EagerHttpProxyConnector's concrete Service::serve method. nightly-2026-07-16 collects both the method and its async closures.

The method may be reaching VtblEntry::Vacant because its predicates are incorrectly considered impossible.

Relevant rama types:

Meta

macOS crash report:

Exception:       EXC_BAD_ACCESS (SIGSEGV)
Subtype:         KERN_INVALID_ADDRESS at 0x0000000000000000
Termination:     Segmentation fault: 11
Faulting thread: tokio-rt-worker
PC:              0x0000000000000000
LR:              0x0000000102511674

The link register resolves to:
rama_http::layer::upgrade::service::
  <UpgradeService<...> as Service<Request>>::serve::{closure#0}
  + 2956
Backtrace
Thread 2 Crashed: tokio-rt-worker

0  0x0000000000000000
1  <UpgradeService<...> as Service<Request>>::serve::{closure#0}
2  <tracing::instrument::Instrumented<
       Trace<...>::serve::{closure#0}
   > as Future>::poll
3  <rama_http_core::proto::h1::dispatch::Server<...>
       as Dispatch>::recv_msg::{closure#0}
4  rama_http_core::proto::h1::dispatch::Dispatcher<...>::poll_write
5  rama_http_core::proto::h1::dispatch::Dispatcher<...>::poll_catch
6  <rama_http_core::server::conn::http1::
       UpgradeableConnection<...> as Future>::poll
7  <rama_http_core::server::conn::auto::
       UpgradeableConnection<...> as Future>::poll
...
13 tokio::runtime::scheduler::multi_thread::worker::Context::run_task
14 tokio::runtime::scheduler::multi_thread::worker::Context::run

The program counter is exactly zero. The link register points inside UpgradeService::serve, at the indirect boxed Service call. Together with the emitted vtable containing zero at method offset 0x18, this indicates that dispatch branches through the null vtable entry.

This may be a recurrence of #152735/#153596 and appears closely related to #158148. It behaviour seems slightly different though: Rust 1.98 silently emits a vacant method slot and the resulting binary crashes, rather than rustc producing an ICE.

Note that this is for some reason the only location where it triggers despite:

  • boxed services being used in many other places
  • that we use that async dynamic trait dispatch trick for several other of our traits as well

Yet somehow only with this trait in this exact example it is failing... So there is hope I am just doing something weird here, as I havent found a stable rustc bug in 11 years that I used Rust in production.... So I'm really hoping to be wrong here...

Found a work around but I would rather not have to add these to the codebase...

Because either I am (a) doing something bad since long and I rather just fix it properly in that case,
as it would mean I was just "lucky" so far. Or this is a real rust bug and in that case I rather help get a 1.98.1 through the door... So far my money is still on me doing something wrong ... somehow... but I don't see how.
AFAIK that code hasn't changed in any way, and it clearly reproduces on rust 1.98 while it does not on previous versions (e.g. 1.97.1) or nightly versions slightly there-after...

Work around
diff --git a/rama-http/src/layer/upgrade/service.rs b/rama-http/src/layer/upgrade/service.rs
index d21adcc8b..eaf030fae 100644
--- a/rama-http/src/layer/upgrade/service.rs
+++ b/rama-http/src/layer/upgrade/service.rs
@@ -125,7 +125,7 @@ pub struct UpgradeHandler<O> {
 
 enum UpgradeHandlerKind<O> {
     ResponseLocal {
-        responder: BoxService<Request, UpgradeOutput<Request, O>, O>,
+        responder: ResponseLocalResponder<O>,
         handler_error_sink: Arc<dyn ErrorSink>,
     },
     SeparateServices {
@@ -149,6 +149,13 @@ struct PreparedUpgrade<O> {
     continuation: UpgradeContinuation,
 }
 
+type ResponseLocalResponderFuture<O> =
+    Pin<Box<dyn Future<Output = Result<UpgradeOutput<Request, O>, O>> + Send + 'static>>;
+// Keep the generic `R: Service` predicate outside the erased call boundary.
+// Rust 1.98 can otherwise emit a vacant `BoxService` vtable entry here.
+type ResponseLocalResponder<O> =
+    Arc<dyn Fn(Request) -> ResponseLocalResponderFuture<O> + Send + Sync + 'static>;
+
 impl<O: Send + 'static> UpgradeHandler<O> {
     /// Register one service which returns its response-local upgrade handler.
     pub(crate) fn new<M, R, Sink>(matcher: M, responder: R, sink: Sink) -> Self
@@ -157,10 +164,15 @@ impl<O: Send + 'static> UpgradeHandler<O> {
         R: Service<Request, Output = UpgradeOutput<Request, O>, Error = O> + Clone,
         Sink: ErrorSink,
     {
+        let responder: ResponseLocalResponder<O> = Arc::new(move |request| {
+            let responder = responder.clone();
+            Box::pin(async move { responder.serve(request).await })
+        });
+
         Self {
             matcher: Box::new(matcher),
             kind: UpgradeHandlerKind::ResponseLocal {
-                responder: responder.boxed(),
+                responder,
                 handler_error_sink: Arc::new(sink),
             },
             _phantom: std::marker::PhantomData,
@@ -203,7 +215,7 @@ impl<O: Send + 'static> UpgradeHandler<O> {
             UpgradeHandlerKind::ResponseLocal {
                 responder,
                 handler_error_sink,
-            } => responder.serve(request).await.map(
+            } => responder(request).await.map(
                 |UpgradeOutput {
                      response,
                      request,

Prior to that I also had a work around which changed all boxed services ,not just this specific use case of it,
I also recall that working. Giving it here as well as extra info:

box service global work around
diff --git a/rama-core/src/service/svc.rs b/rama-core/src/service/svc.rs
--- a/rama-core/src/service/svc.rs
+++ b/rama-core/src/service/svc.rs
@@ -1,5 +1,6 @@
 //! [`Service`] and [`BoxService`] traits.
 
+use core::any::Any;
 use core::convert::Infallible;
 use core::fmt;
 use core::marker::PhantomData;
@@ -145,46 +146,41 @@ where
     }
 }
 
-/// Internal trait for dynamic dispatch of Async Traits,
-/// implemented according to the pioneers of this Design Pattern
-/// found at <https://rust-lang.github.io/async-fundamentals-initiative/evaluation/case-studies/builder-provider-api.html#dynamic-dispatch-behind-the-api>
-/// and widely published at <https://blog.rust-lang.org/inside-rust/2023/05/03/stabilizing-async-fn-in-trait.html>.
-trait DynService<Input> {
-    type Output;
-    type Error;
-
-    #[expect(clippy::type_complexity)]
-    fn serve_box(
-        &self,
-        input: Input,
-    ) -> Pin<Box<dyn Future<Output = Result<Self::Output, Self::Error>> + Send + '_>>;
-}
+// Keep dispatch separate from the erased value's vtable. Rust 1.98 can emit a
+// vacant method slot for the custom async-erasure trait with complex services.
+type ErasedService = dyn Any + Send + Sync + 'static;
+type BoxServiceFuture<'a, Output, Error> =
+    Pin<Box<dyn Future<Output = Result<Output, Error>> + Send + 'a>>;
+type ServeErasedFn<Input, Output, Error> =
+    for<'a> fn(&'a ErasedService, Input) -> BoxServiceFuture<'a, Output, Error>;
 
-impl<Input, T> DynService<Input> for T
+fn serve_erased<'a, Input, Output, Error, T>(
+    inner: &'a ErasedService,
+    input: Input,
+) -> BoxServiceFuture<'a, Output, Error>
 where
-    T: Service<Input>,
+    T: Service<Input, Output = Output, Error = Error>,
 {
-    type Output = T::Output;
-    type Error = T::Error;
-
-    fn serve_box(
-        &self,
-        input: Input,
-    ) -> Pin<Box<dyn Future<Output = Result<Self::Output, Self::Error>> + Send + '_>> {
-        Box::pin(self.serve(input))
-    }
+    #[expect(
+        clippy::unwrap_used,
+        reason = "the constructor stores the same service type as its dispatch function"
+    )]
+    let service = inner.downcast_ref::<T>().unwrap();
+    Box::pin(service.serve(input))
 }
 
 /// A boxed [`Service`], to serve Inputs with,
 /// for where you inputuire dynamic dispatch.
 pub struct BoxService<Input, Output, Error> {
-    inner: Arc<dyn DynService<Input, Output = Output, Error = Error> + Send + Sync + 'static>,
+    inner: Arc<ErasedService>,
+    serve: ServeErasedFn<Input, Output, Error>,
 }
 
 impl<Input, Output, Error> Clone for BoxService<Input, Output, Error> {
     fn clone(&self) -> Self {
         Self {
             inner: self.inner.clone(),
+            serve: self.serve,
         }
     }
 }
@@ -198,6 +194,7 @@ impl<Input, Output, Error> BoxService<Input, Output, Error> {
     {
         Self {
             inner: Arc::new(service),
+            serve: serve_erased::<Input, Output, Error, T>,
         }
     }
 }
@@ -223,7 +220,7 @@ where
 
         input: Input,
     ) -> impl Future<Output = Result<Self::Output, Self::Error>> + Send + '_ {
-        self.inner.serve_box(input)
+        (self.serve)(self.inner.as_ref(), input)
     }
 
     #[inline]
联系我们 contact @ memedata.com