MEDIUM: stick-table: set the track-sc limit at boottime via tune.stick-counters

The number of stick-counter entries usable by track-sc rules is currently
set at build time. There is no good value for this since the vast majority
of users don't need any, most need only a few and rare users need more.
Adding more counters for everyone increases memory and CPU usages for no
reason.

This patch moves the per-session and per-stream arrays to a pool of a size
defined at boot time. This way it becomes possible to set the number of
entries at boot time via a new global setting "tune.stick-counters" that
sets the limit for the whole process. When not set, the MAX_SESS_STR_CTR
value still applies, or 3 if not set, as before.

It is also possible to lower the value to 0 to save a bit of memory if
not used at all.

Note that a few low-level sample-fetch functions had to be protected due
to the ability to use sample-fetches in the global section to set some
variables.
diff --git a/doc/configuration.txt b/doc/configuration.txt
index c5e6d2a..6d1c2d8 100644
--- a/doc/configuration.txt
+++ b/doc/configuration.txt
@@ -1158,6 +1158,7 @@
    - tune.sched.low-latency
    - tune.sndbuf.client
    - tune.sndbuf.server
+   - tune.stick-counters
    - tune.ssl.cachesize
    - tune.ssl.capture-buffer-size
    - tune.ssl.capture-cipherlist-size (deprecated)
@@ -3305,6 +3306,21 @@
   dynamically is expensive, they are cached. The default cache size is set to
   1000 entries.
 
+tune.stick-counters <number>
+  Sets the number of stick-counters that may be tracked at the same time by a
+  connection or a request via "track-sc*" actions in "tcp-request" or
+  "http-request" rules. The defaut value is set at build time by the macro
+  MAX_SESS_STK_CTR, and defaults to 3. With this setting it is possible to
+  change the value and ignore the one passed at build time. Increasing this
+  value may be needed when porting complex configurations to haproxy, but users
+  are warned against the costs: each entry takes 16 bytes per connection and
+  16 bytes per request, all of which need to be allocated and zeroed for all
+  requests even when not used. As such a value of 10 will inflate the memory
+  consumption per request by 320 bytes and will cause this memory to be erased
+  for each request, which does have measurable CPU impacts. Conversely, when
+  no "track-sc" rules are used, the value may be lowered (0 being valid to
+  entirely disable stick-counters).
+
 tune.vars.global-max-size <size>
 tune.vars.proc-max-size <size>
 tune.vars.reqres-max-size <size>
@@ -7617,9 +7633,10 @@
 
   This enables tracking of sticky counters from current request. These rules do
   not stop evaluation and do not change default action. The number of counters
-  that may be simultaneously tracked by the same connection is set in
-  MAX_SESS_STKCTR at build time (reported in haproxy -vv) which defaults to 3,
-  so the track-sc number is between 0 and (MAX_SESS_STKCTR-1). The first
+  that may be simultaneously tracked by the same connection is set by the
+  global "tune.stick-counters" setting, which defaults to MAX_SESS_STKCTR if
+  set at build time (it is reported in haproxy -vv) and which defaults to 3,
+  so the track-sc number is between 0 and (tune.stick-counters-1). The first
   "track-sc0" rule executed enables tracking of the counters of the specified
   table as the first set. The first "track-sc1" rule executed enables tracking
   of the counters of the specified table as the second set. The first
@@ -18924,12 +18941,12 @@
 the incoming connection. For retrieving a value from a sticky counters, the
 counter number can be explicitly set as 0, 1, or 2 using the pre-defined
 "sc0_", "sc1_", or "sc2_" prefix. These three pre-defined prefixes can only be
-used if MAX_SESS_STKCTR value does not exceed 3, otherwise the counter number
-can be specified as the first integer argument when using the "sc_" prefix.
-Starting from "sc_0" to "sc_N" where N is (MAX_SESS_STKCTR-1). An optional
-table may be specified with the "sc*" form, in which case the currently
-tracked key will be looked up into this alternate table instead of the table
-currently being tracked.
+used if the global "tune.stick-counters" value does not exceed 3, otherwise the
+counter number can be specified as the first integer argument when using the
+"sc_" prefix starting from "sc_0" to "sc_N" where N is (tune.stick-counters-1).
+An optional table may be specified with the "sc*" form, in which case the
+currently tracked key will be looked up into this alternate table instead of
+the table currently being tracked.
 
 bc_dst : ip
   This is the destination ip address of the connection on the server side,
diff --git a/include/haproxy/global-t.h b/include/haproxy/global-t.h
index 11f4b2c..92827c6 100644
--- a/include/haproxy/global-t.h
+++ b/include/haproxy/global-t.h
@@ -162,6 +162,7 @@
 		int pool_high_count;  /* max number of opened fd before we start killing idle connections when creating new connections */
 		size_t pool_cache_size;    /* per-thread cache size per pool (defaults to CONFIG_HAP_POOL_CACHE_SIZE) */
 		unsigned short idle_timer; /* how long before an empty buffer is considered idle (ms) */
+		int nb_stk_ctr;       /* number of stick counters, defaults to MAX_SESS_STKCTR */
 #ifdef USE_QUIC
 		unsigned int quic_backend_max_idle_timeout;
 		unsigned int quic_frontend_max_idle_timeout;
diff --git a/include/haproxy/session-t.h b/include/haproxy/session-t.h
index e7a9899..46023f0 100644
--- a/include/haproxy/session-t.h
+++ b/include/haproxy/session-t.h
@@ -50,7 +50,7 @@
 	enum obj_type *origin;          /* the connection / applet which initiated this session */
 	struct timeval accept_date;     /* date of the session's accept() in user date */
 	struct timeval tv_accept;       /* date of the session's accept() in internal date (monotonic) */
-	struct stkctr stkctr[MAX_SESS_STKCTR];  /* stick counters for tcp-connection */
+	struct stkctr *stkctr;          /* stick counters for tcp-connection */
 	struct vars vars;               /* list of variables for the session scope. */
 	struct task *task;              /* handshake timeout processing */
 	long t_handshake;               /* handshake duration, -1 = not completed */
diff --git a/include/haproxy/session.h b/include/haproxy/session.h
index 13152cf..38335e4 100644
--- a/include/haproxy/session.h
+++ b/include/haproxy/session.h
@@ -50,7 +50,10 @@
 	int i;
 	struct stksess *ts;
 
-	for (i = 0; i < MAX_SESS_STKCTR; i++) {
+	if (unlikely(!sess->stkctr)) // pool not allocated yet
+		return;
+
+	for (i = 0; i < global.tune.nb_stk_ctr; i++) {
 		struct stkctr *stkctr = &sess->stkctr[i];
 
 		ts = stkctr_entry(stkctr);
@@ -80,7 +83,10 @@
 {
 	int i;
 
+	if (unlikely(!sess->stkctr)) // pool not allocated yet
+		return;
+
-	for (i = 0; i < MAX_SESS_STKCTR; i++)
+	for (i = 0; i < global.tune.nb_stk_ctr; i++)
 		stkctr_inc_http_req_ctr(&sess->stkctr[i]);
 }
 
@@ -94,7 +100,10 @@
 {
 	int i;
 
-	for (i = 0; i < MAX_SESS_STKCTR; i++)
+	if (unlikely(!sess->stkctr)) // pool not allocated yet
+		return;
+
+	for (i = 0; i < global.tune.nb_stk_ctr; i++)
 		stkctr_inc_http_err_ctr(&sess->stkctr[i]);
 }
 
@@ -107,7 +116,10 @@
 {
 	int i;
 
-	for (i = 0; i < MAX_SESS_STKCTR; i++)
+	if (unlikely(!sess->stkctr)) // pool not allocated yet
+		return;
+
+	for (i = 0; i < global.tune.nb_stk_ctr; i++)
 		stkctr_inc_http_fail_ctr(&sess->stkctr[i]);
 }
 
diff --git a/include/haproxy/stick_table.h b/include/haproxy/stick_table.h
index 196aabb..cbec137 100644
--- a/include/haproxy/stick_table.h
+++ b/include/haproxy/stick_table.h
@@ -32,6 +32,7 @@
 #include <haproxy/ticks.h>
 
 extern struct stktable *stktables_list;
+extern struct pool_head *pool_head_stk_ctr;
 extern struct stktable_type stktable_types[];
 
 #define stktable_data_size(type) (sizeof(((union stktable_data*)0)->type))
diff --git a/include/haproxy/stream-t.h b/include/haproxy/stream-t.h
index 1ec40f5..fe85217 100644
--- a/include/haproxy/stream-t.h
+++ b/include/haproxy/stream-t.h
@@ -245,7 +245,7 @@
 		struct stktable *table;
 	} store[8];                     /* tracked stickiness values to store */
 
-	struct stkctr stkctr[MAX_SESS_STKCTR];  /* content-aware stick counters */
+	struct stkctr *stkctr;                  /* content-aware stick counters */
 
 	struct strm_flt strm_flt;               /* current state of filters active on this stream */
 
diff --git a/include/haproxy/stream.h b/include/haproxy/stream.h
index 06716ae..ac2158d 100644
--- a/include/haproxy/stream.h
+++ b/include/haproxy/stream.h
@@ -116,7 +116,10 @@
 	int i;
 	struct stksess *ts;
 
-	for (i = 0; i < MAX_SESS_STKCTR; i++) {
+	if (unlikely(!s->stkctr)) // pool not allocated yet
+		return;
+
+	for (i = 0; i < global.tune.nb_stk_ctr; i++) {
 		ts = stkctr_entry(&s->stkctr[i]);
 		if (!ts)
 			continue;
@@ -152,7 +155,10 @@
 	void *ptr;
 	int i;
 
+	if (unlikely(!s->stkctr)) // pool not allocated yet
+		return;
+
-	for (i = 0; i < MAX_SESS_STKCTR; i++) {
+	for (i = 0; i < global.tune.nb_stk_ctr; i++) {
 		ts = stkctr_entry(&s->stkctr[i]);
 		if (!ts)
 			continue;
@@ -231,7 +237,10 @@
 {
 	int i;
 
+	if (unlikely(!s->stkctr)) // pool not allocated yet
+		return;
+
-	for (i = 0; i < MAX_SESS_STKCTR; i++) {
+	for (i = 0; i < global.tune.nb_stk_ctr; i++) {
 		if (!stkctr_inc_http_req_ctr(&s->stkctr[i]))
 			stkctr_inc_http_req_ctr(&s->sess->stkctr[i]);
 	}
@@ -244,7 +253,10 @@
 {
 	int i;
 
-	for (i = 0; i < MAX_SESS_STKCTR; i++) {
+	if (unlikely(!s->stkctr)) // pool not allocated yet
+		return;
+
+	for (i = 0; i < global.tune.nb_stk_ctr; i++) {
 		if (!stkctr_entry(&s->stkctr[i]) || !(stkctr_flags(&s->stkctr[i]) & STKCTR_TRACK_BACKEND))
 			continue;
 
@@ -262,7 +274,10 @@
 {
 	int i;
 
+	if (unlikely(!s->stkctr)) // pool not allocated yet
+		return;
+
-	for (i = 0; i < MAX_SESS_STKCTR; i++) {
+	for (i = 0; i < global.tune.nb_stk_ctr; i++) {
 		if (!stkctr_inc_http_err_ctr(&s->stkctr[i]))
 			stkctr_inc_http_err_ctr(&s->sess->stkctr[i]);
 	}
@@ -277,7 +292,10 @@
 {
 	int i;
 
+	if (unlikely(!s->stkctr)) // pool not allocated yet
+		return;
+
-	for (i = 0; i < MAX_SESS_STKCTR; i++) {
+	for (i = 0; i < global.tune.nb_stk_ctr; i++) {
 		if (!stkctr_inc_http_fail_ctr(&s->stkctr[i]))
 			stkctr_inc_http_fail_ctr(&s->sess->stkctr[i]);
 	}
diff --git a/src/cfgparse.c b/src/cfgparse.c
index ac98a95..6f2d93a 100644
--- a/src/cfgparse.c
+++ b/src/cfgparse.c
@@ -1564,9 +1564,13 @@
 		return -1;
 	}
 
-	if (num >= MAX_SESS_STKCTR) {
-		memprintf(errmsg, "%u track-sc number exceeding "
-		          "%d (MAX_SESS_STKCTR-1) value", num, MAX_SESS_STKCTR - 1);
+	if (num >= global.tune.nb_stk_ctr) {
+		if (!global.tune.nb_stk_ctr)
+			memprintf(errmsg, "%u track-sc number not usable, stick-counters "
+			          "are disabled by tune.stick-counters", num);
+		else
+			memprintf(errmsg, "%u track-sc number exceeding "
+			          "%d (tune.stick-counters-1) value", num, global.tune.nb_stk_ctr - 1);
 		return -1;
 	}
 
diff --git a/src/haproxy.c b/src/haproxy.c
index c454c80..5f345f9 100644
--- a/src/haproxy.c
+++ b/src/haproxy.c
@@ -204,6 +204,7 @@
 #else
 		.idle_timer = 1000, /* 1 second */
 #endif
+		.nb_stk_ctr = MAX_SESS_STKCTR,
 #ifdef USE_QUIC
 		.quic_backend_max_idle_timeout = QUIC_TP_DFLT_BACK_MAX_IDLE_TIMEOUT,
 		.quic_frontend_max_idle_timeout = QUIC_TP_DFLT_FRONT_MAX_IDLE_TIMEOUT,
diff --git a/src/session.c b/src/session.c
index 3cbacc7..5c868ae 100644
--- a/src/session.c
+++ b/src/session.c
@@ -46,7 +46,13 @@
 		sess->origin = origin;
 		sess->accept_date = date; /* user-visible date for logging */
 		sess->tv_accept   = now;  /* corrected date for internal use */
-		memset(sess->stkctr, 0, sizeof(sess->stkctr));
+		sess->stkctr = NULL;
+		if (pool_head_stk_ctr) {
+			sess->stkctr = pool_alloc(pool_head_stk_ctr);
+			if (!sess->stkctr)
+				goto out_fail_alloc;
+			memset(sess->stkctr, 0, sizeof(sess->stkctr[0]) * global.tune.nb_stk_ctr);
+		}
 		vars_init_head(&sess->vars, SCOPE_SESS);
 		sess->task = NULL;
 		sess->t_handshake = -1; /* handshake not done yet */
@@ -60,6 +66,9 @@
 		sess->dst = NULL;
 	}
 	return sess;
+ out_fail_alloc:
+	pool_free(pool_head_session, sess);
+	return NULL;
 }
 
 void session_free(struct session *sess)
@@ -70,6 +79,7 @@
 	if (sess->listener)
 		listener_release(sess->listener);
 	session_store_counters(sess);
+	pool_free(pool_head_stk_ctr, sess->stkctr);
 	vars_prune_per_sess(&sess->vars);
 	conn = objt_conn(sess->origin);
 	if (conn != NULL && conn->mux)
@@ -116,7 +126,7 @@
 
 	proxy_inc_fe_sess_ctr(sess->listener, sess->fe);
 
-	for (i = 0; i < MAX_SESS_STKCTR; i++) {
+	for (i = 0; i < global.tune.nb_stk_ctr; i++) {
 		stkctr = &sess->stkctr[i];
 		if (!stkctr_entry(stkctr))
 			continue;
diff --git a/src/stick_table.c b/src/stick_table.c
index 3533ec0..6a86111 100644
--- a/src/stick_table.c
+++ b/src/stick_table.c
@@ -50,7 +50,7 @@
 /* structure used to return a table key built from a sample */
 static THREAD_LOCAL struct stktable_key static_table_key;
 static int (*smp_fetch_src)(const struct arg *, struct sample *, const char *, void *);
-
+struct pool_head *pool_head_stk_ctr __read_mostly = NULL;
 struct stktable *stktables_list;
 struct eb_root stktable_by_name = EB_ROOT;
 
@@ -2456,14 +2456,16 @@
                                        struct session *sess, struct stream *s, int flags)
 {
 	struct stksess *ts;
-	struct stkctr *stkctr;
+	struct stkctr *stkctr = NULL;
 	unsigned int period = 0;
 
 	/* Extract the stksess, return OK if no stksess available. */
-	if (s)
+	if (s && s->stkctr)
 		stkctr = &s->stkctr[rule->arg.gpc.sc];
-	else
+	else if (sess->stkctr)
 		stkctr = &sess->stkctr[rule->arg.gpc.sc];
+	else
+		return ACT_RET_CONT;
 
 	ts = stkctr_entry(stkctr);
 	if (ts) {
@@ -2522,6 +2524,11 @@
 	const char *cmd_name = args[*arg-1];
 	char *error;
 
+	if (!global.tune.nb_stk_ctr) {
+		memprintf(err, "Cannot use '%s', stick-counters are disabled via tune.stick-counters", args[*arg-1]);
+		return ACT_RET_PRS_ERR;
+	}
+
 	cmd_name += strlen("sc-inc-gpc");
 	if (*cmd_name == '(') {
 		cmd_name++; /* skip the '(' */
@@ -2538,9 +2545,9 @@
 				return ACT_RET_PRS_ERR;
 			}
 
-			if (rule->arg.gpc.sc >= MAX_SESS_STKCTR) {
-				memprintf(err, "invalid stick table track ID '%s'. The max allowed ID is %d",
-				          args[*arg-1], MAX_SESS_STKCTR-1);
+			if (rule->arg.gpc.sc >= global.tune.nb_stk_ctr) {
+				memprintf(err, "invalid stick table track ID '%s'. The max allowed ID is %d (tune.stick-counters)",
+				          args[*arg-1], global.tune.nb_stk_ctr-1);
 				return ACT_RET_PRS_ERR;
 			}
 		}
@@ -2566,9 +2573,9 @@
 				return ACT_RET_PRS_ERR;
 			}
 
-			if (rule->arg.gpc.sc >= MAX_SESS_STKCTR) {
-				memprintf(err, "invalid stick table track ID. The max allowed ID is %d",
-				          MAX_SESS_STKCTR-1);
+			if (rule->arg.gpc.sc >= global.tune.nb_stk_ctr) {
+				memprintf(err, "invalid stick table track ID. The max allowed ID is %d (tune.stick-counters)",
+				          global.tune.nb_stk_ctr-1);
 				return ACT_RET_PRS_ERR;
 			}
 		}
@@ -2599,16 +2606,18 @@
 {
 	void *ptr;
 	struct stksess *ts;
-	struct stkctr *stkctr;
+	struct stkctr *stkctr = NULL;
 	unsigned int value = 0;
 	struct sample *smp;
 	int smp_opt_dir;
 
 	/* Extract the stksess, return OK if no stksess available. */
-	if (s)
+	if (s && s->stkctr)
 		stkctr = &s->stkctr[rule->arg.gpt.sc];
-	else
+	else if (sess->stkctr)
 		stkctr = &sess->stkctr[rule->arg.gpt.sc];
+	else
+		return ACT_RET_CONT;
 
 	ts = stkctr_entry(stkctr);
 	if (!ts)
@@ -2663,16 +2672,18 @@
 {
 	void *ptr;
 	struct stksess *ts;
-	struct stkctr *stkctr;
+	struct stkctr *stkctr = NULL;
 	unsigned int value = 0;
 	struct sample *smp;
 	int smp_opt_dir;
 
 	/* Extract the stksess, return OK if no stksess available. */
-	if (s)
+	if (s && s->stkctr)
 		stkctr = &s->stkctr[rule->arg.gpt.sc];
-	else
+	else if (sess->stkctr)
 		stkctr = &sess->stkctr[rule->arg.gpt.sc];
+	else
+		return ACT_RET_CONT;
 
 	ts = stkctr_entry(stkctr);
 	if (!ts)
@@ -2741,6 +2752,11 @@
 	char *error;
 	int smp_val;
 
+	if (!global.tune.nb_stk_ctr) {
+		memprintf(err, "Cannot use '%s', stick-counters are disabled via tune.stick-counters", args[*arg-1]);
+		return ACT_RET_PRS_ERR;
+	}
+
 	cmd_name += strlen("sc-set-gpt");
 	if (*cmd_name == '(') {
 		cmd_name++; /* skip the '(' */
@@ -2757,9 +2773,9 @@
 				return ACT_RET_PRS_ERR;
 			}
 
-			if (rule->arg.gpt.sc >= MAX_SESS_STKCTR) {
+			if (rule->arg.gpt.sc >= global.tune.nb_stk_ctr) {
 				memprintf(err, "invalid stick table track ID '%s'. The max allowed ID is %d",
-				          args[*arg-1], MAX_SESS_STKCTR-1);
+				          args[*arg-1], global.tune.nb_stk_ctr-1);
 				return ACT_RET_PRS_ERR;
 			}
 		}
@@ -2783,9 +2799,9 @@
 				return ACT_RET_PRS_ERR;
 			}
 
-			if (rule->arg.gpt.sc >= MAX_SESS_STKCTR) {
+			if (rule->arg.gpt.sc >= global.tune.nb_stk_ctr) {
 				memprintf(err, "invalid stick table track ID '%s'. The max allowed ID is %d",
-				          args[*arg-1], MAX_SESS_STKCTR-1);
+				          args[*arg-1], global.tune.nb_stk_ctr-1);
 				return ACT_RET_PRS_ERR;
 			}
 		}
@@ -2914,15 +2930,19 @@
 	 * the sc[0-9]_ form, or even higher using sc_(num) if needed.
 	 * args[arg] is the first optional argument. We first lookup the
 	 * ctr form the stream, then from the session if it was not there.
-	 * But we must be sure the counter does not exceed MAX_SESS_STKCTR.
+	 * But we must be sure the counter does not exceed global.tune.nb_stk_ctr.
 	 */
-	if (num >= MAX_SESS_STKCTR)
+	if (num >= global.tune.nb_stk_ctr)
 		return NULL;
 
-	if (strm)
+	stkptr = NULL;
+	if (strm && strm->stkctr)
 		stkptr = &strm->stkctr[num];
-	if (!strm || !stkctr_entry(stkptr)) {
-		stkptr = &sess->stkctr[num];
+	if (!strm || !stkptr || !stkctr_entry(stkptr)) {
+		if (sess->stkctr)
+			stkptr = &sess->stkctr[num];
+		else
+			return NULL;
 		if (!stkctr_entry(stkptr))
 			return NULL;
 	}
@@ -4990,6 +5010,45 @@
 	}
 }
 
+static int stk_parse_stick_counters(char **args, int section_type, struct proxy *curpx,
+                                const struct proxy *defpx, const char *file, int line,
+                                char **err)
+{
+	char *error;
+	int counters;
+
+	counters = strtol(args[1], &error, 10);
+	if (*error != 0) {
+		memprintf(err, "%s: '%s' is an invalid number", args[0], args[1]);
+		return -1;
+	}
+
+	if (counters < 0) {
+		memprintf(err, "%s: the number of stick-counters may not be negative (was %d)", args[0], counters);
+		return -1;
+	}
+
+	global.tune.nb_stk_ctr = counters;
+	return 0;
+}
+
+/* This function creates the stk_ctr pools after the configuration parsing. It
+ * returns 0 on success otherwise ERR_*. If nb_stk_ctr is 0, the pool remains
+ * NULL.
+ */
+static int stkt_create_stk_ctr_pool(void)
+{
+	if (!global.tune.nb_stk_ctr)
+		return 0;
+
+	pool_head_stk_ctr = create_pool("stk_ctr", sizeof(*((struct session*)0)->stkctr) * global.tune.nb_stk_ctr, MEM_F_SHARED);
+	if (!pool_head_stk_ctr) {
+		ha_alert("out of memory while creating the stick-counters pool.\n");
+		return ERR_ABORT;
+	}
+	return 0;
+}
+
 static void stkt_late_init(void)
 {
 	struct sample_fetch *f;
@@ -4997,6 +5056,7 @@
 	f = find_sample_fetch("src", strlen("src"));
 	if (f)
 		smp_fetch_src = f->process;
+	hap_register_post_check(stkt_create_stk_ctr_pool);
 }
 
 INITCALL0(STG_INIT, stkt_late_init);
@@ -5273,3 +5333,10 @@
 }};
 
 INITCALL1(STG_REGISTER, sample_register_convs, &sample_conv_kws);
+
+static struct cfg_kw_list cfg_kws = {{ },{
+	{ CFG_GLOBAL, "tune.stick-counters", stk_parse_stick_counters },
+	{ /* END */ }
+}};
+
+INITCALL1(STG_REGISTER, cfg_register_keywords, &cfg_kws);
diff --git a/src/stream.c b/src/stream.c
index 006c229..04b4081 100644
--- a/src/stream.c
+++ b/src/stream.c
@@ -388,13 +388,20 @@
 	s->last_rule_file = NULL;
 	s->last_rule_line = 0;
 
-	/* Copy SC counters for the stream. We don't touch refcounts because
-	 * any reference we have is inherited from the session. Since the stream
-	 * doesn't exist without the session, the session's existence guarantees
-	 * we don't lose the entry. During the store operation, the stream won't
-	 * touch these ones.
-	 */
-	memcpy(s->stkctr, sess->stkctr, sizeof(s->stkctr));
+	s->stkctr = NULL;
+	if (pool_head_stk_ctr) {
+		s->stkctr = pool_alloc(pool_head_stk_ctr);
+		if (!s->stkctr)
+			goto out_fail_alloc;
+
+		/* Copy SC counters for the stream. We don't touch refcounts because
+		 * any reference we have is inherited from the session. Since the stream
+		 * doesn't exist without the session, the session's existence guarantees
+		 * we don't lose the entry. During the store operation, the stream won't
+		 * touch these ones.
+		 */
+		memcpy(s->stkctr, sess->stkctr, sizeof(s->stkctr[0]) * global.tune.nb_stk_ctr);
+	}
 
 	s->sess = sess;
 
@@ -582,6 +589,8 @@
  out_fail_attach_scf:
 	task_destroy(t);
  out_fail_alloc:
+	if (s)
+		pool_free(pool_head_stk_ctr, s->stkctr);
 	pool_free(pool_head_stream, s);
 	DBG_TRACE_DEVEL("leaving on error", STRM_EV_STRM_NEW|STRM_EV_STRM_ERR);
 	return NULL;
@@ -701,6 +710,7 @@
 		vars_prune(&s->vars_reqres, s->sess, s);
 
 	stream_store_counters(s);
+	pool_free(pool_head_stk_ctr, s->stkctr);
 
 	list_for_each_entry_safe(bref, back, &s->back_refs, users) {
 		/* we have to unlink all watchers. We must not relink them if
@@ -797,7 +807,7 @@
 		if (sess->listener && sess->listener->counters)
 			_HA_ATOMIC_ADD(&sess->listener->counters->bytes_in, bytes);
 
-		for (i = 0; i < MAX_SESS_STKCTR; i++) {
+		for (i = 0; i < global.tune.nb_stk_ctr; i++) {
 			if (!stkctr_inc_bytes_in_ctr(&s->stkctr[i], bytes))
 				stkctr_inc_bytes_in_ctr(&sess->stkctr[i], bytes);
 		}
@@ -815,7 +825,7 @@
 		if (sess->listener && sess->listener->counters)
 			_HA_ATOMIC_ADD(&sess->listener->counters->bytes_out, bytes);
 
-		for (i = 0; i < MAX_SESS_STKCTR; i++) {
+		for (i = 0; i < global.tune.nb_stk_ctr; i++) {
 			if (!stkctr_inc_bytes_out_ctr(&s->stkctr[i], bytes))
 				stkctr_inc_bytes_out_ctr(&sess->stkctr[i], bytes);
 		}
diff --git a/src/tcp_rules.c b/src/tcp_rules.c
index e649794..6465e6e 100644
--- a/src/tcp_rules.c
+++ b/src/tcp_rules.c
@@ -1058,7 +1058,7 @@
 			memprintf(err,
 			          "'%s %s' expects 'accept', 'reject', 'capture', 'expect-proxy', 'expect-netscaler-cip', 'track-sc0' ... 'track-sc%d', %s "
 			          "in %s '%s' (got '%s').%s%s%s\n",
-			          args[0], args[1], MAX_SESS_STKCTR-1,
+			          args[0], args[1], global.tune.nb_stk_ctr-1,
 			          trash.area, proxy_type_str(curpx),
 			          curpx->id, args[arg],
 			          best ? " Did you mean '" : "",