Skip to content

Steer built with zero services returns poll_ready = Ready(Ok(())) but call panics #859

Description

@meng-xu-cs

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();
    // ...
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions