On Sun, Sep 13, 2026 at 1:50 AM Julian Anastasov <[email protected]> wrote: > > > Hello, > > On Sat, 12 Sep 2026, Zihan Xi wrote: > > > Hi Linux kernel maintainers, > > > > We found and validated an issue triggered through > > net/netfilter/ipvs/ip_vs_ftp.c. An unprivileged user can trigger it by > > creating a user namespace and a network namespace. We tested the fix with > > the same trigger. The change applies to the generic > > ip_vs_conn_expire() control-chain cleanup path. For the reported trigger, > > it only defers recursive controller expiration; testing showed no change to > > other IPVS behavior. > > > > We will provide detailed information about the bug > > in this email, along with a PoC to trigger it. > > > > ---- details below ---- > > > > Bug details: > > > > The trigger entry point is ip_vs_ftp_out() in > > net/netfilter/ipvs/ip_vs_ftp.c. It parses an EPSV reply and creates a > > wildcard data connection using the advertised port. If that port is 21, > > ip_vs_conn_new() binds the new connection to the FTP helper a second time, > > because 21 is the helper's control port. The connection has > > IP_VS_CONN_F_NO_CPORT, so the next connection from the same client to the > > VIP on port 21 matches the wildcard entry instead of creating a new > > top-level entry. Repeating EPSV builds a chain of controlled connections. > > Hm, may be we should also avoid such long chains. > Probably in separate patch, for example: > > diff --git a/net/netfilter/ipvs/ip_vs_ftp.c b/net/netfilter/ipvs/ip_vs_ftp.c > index 9e3e005a8263..0622169b5970 100644 > --- a/net/netfilter/ipvs/ip_vs_ftp.c > +++ b/net/netfilter/ipvs/ip_vs_ftp.c > @@ -237,6 +237,17 @@ static int ip_vs_ftp_get_addrport(char *data, char > *data_limit, > return 1; > } > > +static bool is_control_port(u16 port) > +{ > + int i; > + > + for (i = 0; i < ports_count; i++) { > + if (ports[i] == port) > + return true; > + } > + return false; > +} > + > /* Look at outgoing ftp packets to catch the response to a PASV/EPSV command > * from the server (inside-to-outside). > * When we see one, we build a connection entry with the client address, > @@ -293,6 +304,9 @@ static int ip_vs_ftp_out(struct ip_vs_app *app, struct > ip_vs_conn *cp, > > IP_VS_DBG(7, "PASV response (%pI4:%u) -> %pI4:%u detected\n", > &from.ip, ntohs(port), &cp->caddr.ip, 0); > + /* Do not redirect data to control ports */ > + if (!port || is_control_port(ntohs(port))) > + return 0; > } else if (cp->app_data == (void *) IP_VS_FTP_EPSV) { > data = ip_vs_ftp_data_ptr(skb, ipvsh); > data_limit = skb_tail_pointer(skb); > @@ -529,6 +543,9 @@ static int ip_vs_ftp_in(struct ip_vs_app *app, struct > ip_vs_conn *cp, > return 1; > } > > + if (!port) > + return 0; > + > /* Passive mode off */ > cp->app_data = (void *) IP_VS_FTP_ACTIVE; > > The only problem I see with your proposed change is > that we may need 2-3 timer ticks to expire a DATA->CTL->TPL > chain. Or it expires on the same tick? > > Alternative would be to jump to the beginnig of the > function after successful timer_delete() for our cp->control, > i.e. to use loop instead of recursion. I.e. ip_vs_conn_del_put() > can be converted to function that returns bool instead of > calling ip_vs_conn_expire(), so that we can know if to loop. > > If ip_vs_conn_flush() demands faster expiring, a > loop will work faster. What do you think? > > Regards > > -- > Julian Anastasov <[email protected]> >
Hello Julian, Thanks for reviewing this and for suggesting the FTP helper change. I agree that a loop is preferable, especially if ip_vs_conn_flush() needs to expire the controller chain promptly. The follow-up is split into two separate patches. The first patch changes ip_vs_conn_del_put() to report whether it successfully deleted the controller's timer. ip_vs_conn_expire() then follows the controller through a repeat path instead of recursively invoking itself. This keeps cleanup synchronous, avoids recursive stack growth, and does not wait for additional timer ticks. The second patch is a separate FTP helper fix that prevents the helper from creating unnecessarily long controller chains. ip_vs_ftp_out() rejects a zero port and configured control ports in the passive-data path, covering both PASV and EPSV. ip_vs_ftp_in() also rejects a zero client port in active mode. Best regards, Zihan Xi

