Skip to content

server-state-file silently overrides config port on reload for statically configured servers #3296

@igorwwwwwwwwwwwwwwwwwwww

Description

Detailed Description of the Problem

When using server-state-file / load-server-state-from-file global, changing a server's port in the configuration and reloading HAProxy causes the old port from the state file to be silently applied instead of the new port from the configuration.

In src/server_state.c, srv_state_srv_update() unconditionally overwrites srv->svc_port with the value from the state file:

if (port_st)
    srv->svc_port = port_svc;

This is inconsistent with how weight is handled in the same function, where the state file value is only applied if the configuration hasn't changed:

if (srv_iweight == srv->iweight) {
    srv->uweight = srv_uweight;
}

There is no equivalent guard for the port. Since apply_server_state() runs before srv_init_addr() in src/haproxy.c, and srv_init_addr() only touches the address (not the port), nothing corrects the stale port afterwards.

Expected Behavior

When the server port is changed in the configuration and HAProxy is reloaded, the new config port should take effect. The state file should not silently override it — matching the existing behavior for weight.

Steps to Reproduce the Behavior

Reproducer script (requires socat):

#!/bin/bash
# Repro: state file overrides config port on reload (no config-change guard)
# src/server_state.c: if (port_st) srv->svc_port = port_svc;
set -euo pipefail

HAP="${HAPROXY:-haproxy}"
D=$(mktemp -d /tmp/haproxy-port-bug.XXXXXX)
trap 'kill $(cat "$D/h.pid" 2>/dev/null) 2>/dev/null; rm -rf "$D"' EXIT

mk_cfg() {
cat > "$D/h.cfg" <<EOF
global
  stats socket $D/h.sock level admin expose-fd listeners
  server-state-file $D/state
defaults
  mode http
  timeout client 5s
  timeout connect 5s
  timeout server 5s
  load-server-state-from-file global
listen t
  bind 127.0.0.1:18080
  http-request return status 200
backend b
  server s1 127.0.0.1:$1 init-addr 127.0.0.1
EOF
}

port_of() { echo "show servers state" | socat - "$D/h.sock" | awk '/^[0-9].* /{print $19;exit}'; }

$HAP -v | head -1

# start with port 9001, master-worker mode for USR2 reload
mk_cfg 9001
$HAP -f "$D/h.cfg" -p "$D/h.pid" -W 2>"$D/err" &
sleep 0.5
P=$(port_of); echo "1) after start:  port=$P (expect 9001)"

# save state, change config to port 9002, reload
echo "show servers state" | socat - "$D/h.sock" > "$D/state"
mk_cfg 9002
kill -USR2 $(cat "$D/h.pid"); sleep 0.5
P=$(port_of); echo "2) after reload:  port=$P (expect 9002)"

# dump any warnings for context
grep -i "warning\|ignoring" "$D/err" | sed 's/^/   /' || true

[ "$P" = "9002" ] && echo "OK: config port applied" && exit 0
echo "BUG: state file port 9001 overrode config port 9002"
exit 1

Output on unpatched HAProxy:

HAProxy version 3.3.2-72df9192b 2026/01/29 - https://haproxy.org/
1) after start:  port=9001 (expect 9001)
2) after reload:  port=9001 (expect 9002)
   [WARNING]  (87686) : Proxy t stopped (cumulated conns: FE: 0, BE: 0).
   [WARNING]  (87686) : Proxy b stopped (cumulated conns: FE: 0, BE: 0).
   [WARNING]  (87684) : Former worker (87686) exited with code 0 (Exit)
BUG: state file port 9001 overrode config port 9002

Do you have any idea what may have caused this?

The state file port restore in srv_state_srv_update() was written to support DNS SRV record resolution, where the port is dynamically discovered and needs to persist across reloads. But the code doesn't distinguish between "port came from SRV resolution at runtime" and "port was set statically in the config", so it unconditionally overwrites.

Do you have an idea how to solve the issue?

Gate the state file port restore on srv->srvrq being non-null, which is only set for servers using SRV record resolution. For statically configured servers, srv->svc_port is already set from config parsing and should not be overwritten.

diff --git a/src/server_state.c b/src/server_state.c
index 5d7573104..3ec32b447 100644
--- a/src/server_state.c
+++ b/src/server_state.c
@@ -438,7 +438,13 @@ static void srv_state_srv_update(struct server *srv, int version, char **params)
 		srv->flags &= ~SRV_F_MAPPORTS;
 	}
 
-	if (port_st)
+	/* Only restore the port from the state file for SRV-managed servers,
+	 * where the port is dynamically resolved from DNS SRV records.
+	 * For statically configured servers, the config port (already in
+	 * srv->svc_port) must take precedence, otherwise a config change
+	 * would be silently ignored on reload.
+	 */
+	if (port_st && srv->srvrq)
 		srv->svc_port = port_svc;

What is your configuration?

global
  stats socket /tmp/h.sock level admin expose-fd listeners
  server-state-file /tmp/state

defaults
  mode http
  timeout client 5s
  timeout connect 5s
  timeout server 5s
  load-server-state-from-file global

listen t
  bind 127.0.0.1:18080
  http-request return status 200

backend b
  server s1 127.0.0.1:9001

Output of haproxy -vv

HAProxy version 3.3.2-72df9192b 2026/01/29 - https://haproxy.org/
Status: stable branch - will stop receiving fixes around Q1 2027.
Known bugs: http://www.haproxy.org/bugs/bugs-3.3.2.html
Running on: Darwin 25.2.0 Darwin Kernel Version 25.2.0: Tue Nov 18 21:09:55 PST 2025; root:xnu-12377.61.12~1/RELEASE_ARM64_T8103 arm64
Build options :
  TARGET  = osx
  CC      = cc
  CFLAGS  = -O2 -g -fwrapv
  OPTIONS = USE_OPENSSL=1 USE_ZLIB=1 USE_PCRE2=1 USE_PCRE2_JIT=1
  DEBUG   =

Feature list : -51DEGREES -ACCEPT4 -BACKTRACE -CLOSEFROM +CPU_AFFINITY -CRYPT_H -DEVICEATLAS -DL -ECH -ENGINE -EPOLL -EVPORTS +GETADDRINFO +KQUEUE -KTLS -LIBATOMIC +LIBCRYPT -LINUX_CAP -LINUX_SPLICE -LINUX_TPROXY -LUA -MATH -MEMORY_PROFILING -NETFILTER -NS -OBSOLETE_LINKER +OPENSSL -OPENSSL_AWSLC -OPENSSL_WOLFSSL -OT -PCRE +PCRE2 +PCRE2_JIT -PCRE_JIT +POLL -PRCTL -PROCCTL -PROMEX -PTHREAD_EMULATION -QUIC -QUIC_OPENSSL_COMPAT -RT -SHM_OPEN -SLZ +SSL -STATIC_PCRE -STATIC_PCRE2 -TFO +THREAD -THREAD_DUMP +TPROXY -WURFL +ZLIB +ACME

Default settings :
  bufsize = 16384, maxrewrite = 1024, maxpollevents = 200

Built with multi-threading support (MAX_TGROUPS=32, MAX_THREADS=1024, default=8).
Built with SSL library version : OpenSSL 3.6.1 27 Jan 2026
Running on SSL library version : OpenSSL 3.6.1 27 Jan 2026
SSL library supports TLS extensions : yes
SSL library supports SNI : yes
SSL library supports : TLSv1.0 TLSv1.1 TLSv1.2 TLSv1.3
OpenSSL providers loaded : default
Built with zlib version : 1.2.12
Running on zlib version : 1.2.12
Compression algorithms supported : identity("identity"), deflate("deflate"), raw-deflate("deflate"), gzip("gzip")
Built with transparent proxy support using:
Built with PCRE2 version : 10.47 2025-10-21
PCRE2 library supports JIT : yes
Encrypted password support via crypt(3): yes
Built with clang compiler version 17.0.0 (clang-1700.6.3.2)

Available polling systems :
     kqueue : pref=300,  test result OK
       poll : pref=200,  test result OK
     select : pref=150,  test result OK
Total: 3 (3 usable), will use kqueue.

Available multiplexer protocols :
(protocols marked as <default> cannot be specified using 'proto' keyword)
         h2 : mode=HTTP  side=FE|BE  mux=H2    flags=HTX|HOL_RISK|NO_UPG
         h1 : mode=HTTP  side=FE|BE  mux=H1    flags=HTX|NO_UPG
  <default> : mode=HTTP  side=FE|BE  mux=H1    flags=HTX
       fcgi : mode=HTTP  side=BE     mux=FCGI  flags=HTX|HOL_RISK|NO_UPG
       spop : mode=SPOP  side=BE     mux=SPOP  flags=HOL_RISK|NO_UPG
  <default> : mode=SPOP  side=BE     mux=SPOP  flags=HOL_RISK|NO_UPG
       none : mode=TCP   side=FE|BE  mux=PASS  flags=NO_UPG
  <default> : mode=TCP   side=FE|BE  mux=PASS  flags=

Available services : none

Available filters :
        [BWLIM] bwlim-in
        [BWLIM] bwlim-out
        [CACHE] cache
        [COMP] compression
        [FCGI] fcgi-app
        [SPOE] spoe
        [TRACE] trace


Confirmed on 2.8.16, 3.3.4, and current `master` (same code path).

Last Outputs and Backtraces


Additional Information

This bug was discovered while working on server state persistence for GitLab's HAProxy fleet. We had previously implemented init-addr <config-ip> to prevent stale IP addresses from being restored from the state file (see this blog post), but found that port changes in the configuration were still being silently ignored on reload.

Metadata

Metadata

Assignees

No one assigned

    Labels

    status: needs-triageThis issue needs to be triaged.type: bugThis issue describes a bug.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions