From a88404cb8861ceb66304e3e69ca027627ecdd808 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Mon, 31 Aug 2026 11:14:17 -0700 Subject: [PATCH] Drop a forward the peer refused twice A cancel refusal leaves the forward standing, since the peer keeps a listener it would not drop. There is no listener to keep when the setup was refused too, so FwdRemoteSettle() unwinds a registration nothing establishes and nothing is still owed an answer on, rather than leaving it unconfirmed and unmatchable until the session ends. - A cancel refusal unlinks an unconfirmed forward with no setup queued - Test refuses a want-reply setup and the cancel behind it, and checks the registration is gone --- src/internal.c | 6 +++++- tests/regress.c | 31 +++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/internal.c b/src/internal.c index f2924865..72aa6300 100644 --- a/src/internal.c +++ b/src/internal.c @@ -4097,7 +4097,11 @@ static void FwdRemoteSettle(WOLFSSH* ssh, WOLFSSH_FWD_REMOTE* entry, if (isCancel) { if (!success) { /* The peer kept the listener, so the forward stands and matching - * resumes unless a later cancel is outstanding. */ + * resumes unless a later cancel is outstanding. An unconfirmed + * forward has no listener to keep, though: its setup was refused + * too, and with none still queued nothing will ever bind it. */ + if (!entry->confirmed && !FwdReplyHasSetup(ssh, entry)) + FwdRemoteUnlink(ssh, ssh->ctx->heap, entry); return; } diff --git a/tests/regress.c b/tests/regress.c index 548a0fe9..c3fd4e0d 100644 --- a/tests/regress.c +++ b/tests/regress.c @@ -4051,6 +4051,36 @@ static void TestForwardedTcpipCancelBeforeSetupReply(void) FreeChannelOpenHarness(&harness); } +/* The same overlap with both requests refused. The setup bound no listener, + * so the cancel's refusal is only the peer saying it has none to drop, and + * nothing is left to hold the registration open. */ +static void TestForwardedTcpipSetupAndCancelBothRefusedDrops(void) +{ + ChannelOpenHarness harness; + + InitFwdRemoteHarness(&harness); + + AssertIntEQ(wolfSSH_FwdRemoteSetup(harness.ssh, "127.0.0.1", 8080, 1), + WS_SUCCESS); + AssertIntEQ(wolfSSH_FwdRemoteCancel(harness.ssh, "127.0.0.1", 8080, 1), + WS_SUCCESS); + + /* The setup is refused, but the queued cancel still names the forward, so + * it is held for that answer. */ + FeedRequestFailure(&harness); + AssertNotNull(harness.ssh->fwdRemoteList); + + /* The cancel is refused too. Nothing establishes the forward and nothing + * is owed an answer on it, so it goes rather than sitting unmatchable + * until the session ends. */ + FeedRequestFailure(&harness); + AssertNull(harness.ssh->fwdRemoteList); + + AssertForwardedOpenRefused(&harness, "127.0.0.1", 8080); + + FreeChannelOpenHarness(&harness); +} + /* The same overlap, but with a second forward outstanding behind it. The * cancelled forward's reply must not be spent on the one still waiting. */ static void TestForwardedTcpipCancelBeforeSetupReplyKeepsOther(void) @@ -10800,6 +10830,7 @@ int main(int argc, char** argv) TestForwardedTcpipUnmatchedCancelKeepsForward(); TestForwardedTcpipCancelBeforeSetupReply(); TestForwardedTcpipCancelBeforeSetupReplyKeepsOther(); + TestForwardedTcpipSetupAndCancelBothRefusedDrops(); TestForwardedTcpipDuplicateSetupIsOneForward(); TestForwardedTcpipDuplicateSetupRefusalKeepsForward(); TestForwardedTcpipDuplicateSetupLaterSuccessBinds();