Summary
Steer::new accepts an empty service list. In that case, not_ready is also initialized as empty, so Steer::poll_ready immediately returns Poll::Ready(Ok(())).
However, Steer::call still asks the Picker for an index and then unconditionally indexes self.services[idx]. With zero services, there is no valid index, so the first call path panics.
That means Steer can externally report “ready to accept a request” even though every request path is guaranteed to panic.
Reproduction
use std::{
convert::Infallible,
future::{poll_fn, ready, Ready},
panic::AssertUnwindSafe,
task::{Context, Poll},
};
use tower::{steer::Steer, Service};
#[derive(Clone)]
struct Dummy;
impl Service<()> for Dummy {
type Response = ();
type Error = Infallible;
type Future = Ready<Result<(), Infallible>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, _req: ()) -> Self::Future {
ready(Ok(()))
}
}
#[tokio::test]
async fn empty_steer_reports_ready_then_panics() {
let mut steer = Steer::<Dummy, _, ()>::new(Vec::new(), |_: &(), _: &[Dummy]| 0);
// Reports ready even though there is no routable service.
assert!(poll_fn(|cx| steer.poll_ready(cx)).await.is_ok());
// Panics while indexing `self.services[idx]`.
let panicked = std::panic::catch_unwind(AssertUnwindSafe(|| {
let _ = steer.call(());
}))
.is_err();
assert!(panicked);
}
In practice, this is likely a configuration / setup bug rather than a remotely triggerable issue by itself. Therefore, it is not really a security concern yet, just be cautious that it might still crash applications that build Steer from dynamic state.
Suggested fix
Reject empty service lists at construction time, for example:
pub fn new(services: impl IntoIterator<Item = S>, router: F) -> Self {
let services: Vec<_> = services.into_iter().collect();
assert!(!services.is_empty(), "Steer requires at least one service");
let not_ready: VecDeque<_> = services.iter().enumerate().map(|(i, _)| i).collect();
// ...
}
Summary
Steer::newaccepts an empty service list. In that case,not_readyis also initialized as empty, soSteer::poll_readyimmediately returnsPoll::Ready(Ok(())).However,
Steer::callstill asks thePickerfor an index and then unconditionally indexesself.services[idx]. With zero services, there is no valid index, so the firstcallpath panics.That means
Steercan externally report “ready to accept a request” even though every request path is guaranteed to panic.Reproduction
In practice, this is likely a configuration / setup bug rather than a remotely triggerable issue by itself. Therefore, it is not really a security concern yet, just be cautious that it might still crash applications that build
Steerfrom dynamic state.Suggested fix
Reject empty service lists at construction time, for example: