Package: liblttng-ust1t64
Version: 2.13.9-1
Severity: important

Dear Maintainer,

when using LTTng-UST in a trixie container, the RLIMIT_NOFILE is
extremely high (1073741816), which results in 128 MB of RSS memory that
is mapped into each process that links liblttng-ust1t64. Note, that
this effect is independent of a running tracing session.

This already has been reported upstream [1] and a fix is available as
well. I backported the fix to 2.13.9-1 (attached). Please consider
creating a new stable release, as this behavior is definitely
unexpected and in many case breaks the container memory limits.

[1] https://bugs.lttng.org/issues/1438

Best regards,
Felix Moessbauer

Siemens AG
Linux Expert Center
Friedrich-Ludwig-Bauer-Str. 3
85748 Garching, Germany

-- System Information:
Debian Release: 13.3
  APT prefers stable-updates
  APT policy: (500, 'stable-updates'), (500, 'stable-security'), (500,
'stable')
Architecture: amd64 (x86_64)

Kernel: Linux 7.1.13+deb14-amd64 (SMP w/16 CPU threads; PREEMPT)
Kernel taint flags: TAINT_WARN
Locale: LANG=C, LC_CTYPE=C.UTF-8 (charmap=UTF-8), LANGUAGE not set
Shell: /bin/sh linked to /usr/bin/dash
Init: unable to detect

Versions of packages liblttng-ust1t64 depends on:
ii  libc6                    2.41-12+deb13u4
ii  liblttng-ust-common1t64  2.13.9-1
ii  liblttng-ust-ctl5t64     2.13.9-1
ii  libnuma1                 2.0.19-1

liblttng-ust1t64 recommends no packages.

liblttng-ust1t64 suggests no packages.

-- no debconf information
From 70b3920c7acd37def7a04d3946c803f09a802565 Mon Sep 17 00:00:00 2001
From: Olivier Dion <[email protected]>
Date: Wed, 1 Apr 2026 15:11:11 -0400
Subject: [PATCH 1/1] fix: excessive memory usage of fd-tracker

Issue
=====

The fd tracker allocates an array of fd_set structures sized according
to the RLIMIT_NOFILE hard limit. On systems where this limit is very
high (e.g., Docker containers with systemd v240+ where fs.nr_open can be
1,073,741,816), this results in ~128 MB of memory allocation per
process.

Solution
========

Replace the fd_set array with a hash table using cds_hlist from
liburcu. The hash table uses 1024 fixed buckets and dynamically
allocates entries as more file descriptors are tracked.

Memory comparison for typical usage (10 tracked fds):
  - Before: ~128 MB
  - After:  ~8 KB (fixed) + ~240 bytes (10 entries)

The hash table provides O(1) average-case lookup, insertion, and
deletion, matching the performance characteristics of the previous
fd_set implementation.

Known drawbacks
===============

The operations in the "close range" family can be slower in certain
configuration due to lookups in the hash table.

Fix: https://bugs.lttng.org/issues/1438
Signed-off-by: Olivier Dion <[email protected]>
Signed-off-by: Mathieu Desnoyers <[email protected]>
Change-Id: I16c7b48797e25a72737da9c14d2b702f96e75767
Signed-off-by: Felix Moessbauer <[email protected]>
---
 src/lib/lttng-ust-common/fd-tracker.c | 197 ++++++++++++++++----------
 1 file changed, 123 insertions(+), 74 deletions(-)

diff --git a/src/lib/lttng-ust-common/fd-tracker.c b/src/lib/lttng-ust-common/fd-tracker.c
index 102e5c05..88d94889 100644
--- a/src/lib/lttng-ust-common/fd-tracker.c
+++ b/src/lib/lttng-ust-common/fd-tracker.c
@@ -14,7 +14,6 @@
 #include <assert.h>
 #include <errno.h>
 #include <fcntl.h>
-#include <sys/select.h>
 #include <sys/resource.h>
 #include <sys/time.h>
 #include <fcntl.h>
@@ -22,6 +21,7 @@
 #include <signal.h>
 #include <stdbool.h>
 #include <urcu/compiler.h>
+#include <urcu/hlist.h>
 #include <urcu/tls-compat.h>
 #include <urcu/system.h>
 
@@ -33,24 +33,21 @@
 
 #include "lib/lttng-ust-common/fd-tracker.h"
 
-/* Operations on the fd set. */
-#define IS_FD_VALID(fd)			((fd) >= 0 && (fd) < lttng_ust_max_fd)
-#define GET_FD_SET_FOR_FD(fd, fd_sets)	(&((fd_sets)[(fd) / FD_SETSIZE]))
-#define CALC_INDEX_TO_SET(fd)		((fd) % FD_SETSIZE)
-#define IS_FD_STD(fd)			(IS_FD_VALID(fd) && (fd) <= STDERR_FILENO)
+/* Hash table for tracking fds used by lttng-ust. */
+#define FD_TRACKER_HT_BITS	10U
+#define FD_TRACKER_HT_SIZE	(1U << FD_TRACKER_HT_BITS)
 
-/* Check fd validity before calling these. */
-#define ADD_FD_TO_SET(fd, fd_sets)	\
-		FD_SET(CALC_INDEX_TO_SET(fd), GET_FD_SET_FOR_FD(fd, fd_sets))
-#define IS_FD_SET(fd, fd_sets)		\
-		FD_ISSET(CALC_INDEX_TO_SET(fd), GET_FD_SET_FOR_FD(fd, fd_sets))
-#define DEL_FD_FROM_SET(fd, fd_sets)	\
-		FD_CLR(CALC_INDEX_TO_SET(fd), GET_FD_SET_FOR_FD(fd, fd_sets))
+#define IS_FD_STD(fd)		((fd) >= 0 && (fd) <= STDERR_FILENO)
+
+struct lttng_fd_entry {
+	struct cds_hlist_node hlist;
+	int fd;
+};
 
 /*
- * Protect the lttng_fd_set. Nests within the ust_lock, and therefore
- * within the libc dl lock. Therefore, we need to allocate the TLS before
- * nesting into this lock.
+ * Protect fd_tracker_ht. Nests within the ust_lock, and therefore within the
+ * libc dl lock. Therefore, we need to allocate the TLS before nesting into this
+ * lock.
  *
  * The ust_safe_guard_fd_mutex nests within the ust_mutex. This mutex
  * is also held across fork.
@@ -65,12 +62,40 @@ static pthread_mutex_t ust_safe_guard_fd_mutex = PTHREAD_MUTEX_INITIALIZER;
  */
 static DEFINE_URCU_TLS(int, ust_fd_mutex_nest);
 
-/* fd_set used to book keep fd being used by lttng-ust. */
-static fd_set *lttng_fd_set;
-static int lttng_ust_max_fd;
-static int num_fd_sets;
+/* Hash table used to book keep fd being used by lttng-ust. */
+static struct cds_hlist_head fd_tracker_ht[FD_TRACKER_HT_SIZE];
+static unsigned int lttng_ust_max_fd;
 static int init_done;
 
+static inline struct cds_hlist_head *fd_tracker_get_bucket(int fd)
+{
+	return &fd_tracker_ht[(unsigned int) fd % FD_TRACKER_HT_SIZE];
+}
+
+static struct lttng_fd_entry *fd_tracker_lookup(int fd)
+{
+	const struct cds_hlist_head *head;
+	const struct cds_hlist_node *node;
+	struct lttng_fd_entry *entry;
+
+	if (caa_unlikely(fd < 0))
+		return NULL;
+
+	head = fd_tracker_get_bucket(fd);
+
+	cds_hlist_for_each_entry(entry, node, head, hlist) {
+		if (entry->fd == fd)
+			return entry;
+	}
+
+	return NULL;
+}
+
+static inline bool fd_tracker_is_fd_tracked(int fd)
+{
+	return fd_tracker_lookup(fd) != NULL;
+}
+
 /*
  * Force a read (imply TLS allocation for dlopen) of TLS variables.
  */
@@ -80,41 +105,43 @@ void lttng_ust_fd_tracker_alloc_tls(void)
 }
 
 /*
- * Allocate the fd set array based on the hard limit set for this
- * process. This will be called during the constructor execution
+ * Initialize the fd tracker hash table.
+ * This will be called during the constructor execution
  * and will also be called in the child after fork via lttng_ust_init.
  */
 void lttng_ust_fd_tracker_init(void)
 {
-	struct rlimit rlim;
-	int i;
+	size_t i;
 
 	if (CMM_LOAD_SHARED(init_done))
 		return;
 
-	memset(&rlim, 0, sizeof(rlim));
-	/* Get the current possible max number of fd for this process. */
-	if (getrlimit(RLIMIT_NOFILE, &rlim) < 0)
-		abort();
-	/*
-	 * FD set array size determined using the hard limit. Even if
-	 * the process wishes to increase its limit using setrlimit, it
-	 * can only do so with the softlimit which will be less than the
-	 * hard limit.
-	 */
-	lttng_ust_max_fd = rlim.rlim_max;
-	num_fd_sets = lttng_ust_max_fd / FD_SETSIZE;
-	if (lttng_ust_max_fd % FD_SETSIZE)
-		++num_fd_sets;
-	if (lttng_fd_set != NULL) {
-		free(lttng_fd_set);
-		lttng_fd_set = NULL;
+
+	if (!lttng_ust_max_fd) {
+		struct rlimit rlim;
+
+		if (getrlimit(RLIMIT_NOFILE, &rlim) < 0)
+			abort();
+
+		lttng_ust_max_fd = rlim.rlim_max;
+	}
+
+	for (i = 0; i < FD_TRACKER_HT_SIZE; ++i) {
+		struct cds_hlist_head *head;
+		struct cds_hlist_node *node, *tmp;
+		struct lttng_fd_entry *entry;
+
+		head = &fd_tracker_ht[i];
+
+		/* Clear any existing entries (can happen after fork). */
+		cds_hlist_for_each_entry_safe(entry, node, tmp, head, hlist) {
+			cds_hlist_del(&entry->hlist);
+			free(entry);
+		}
+
+		CDS_INIT_HLIST_HEAD(head);
 	}
-	lttng_fd_set = malloc(num_fd_sets * (sizeof(fd_set)));
-	if (!lttng_fd_set)
-		abort();
-	for (i = 0; i < num_fd_sets; i++)
-		FD_ZERO((&lttng_fd_set[i]));
+
 	CMM_STORE_SHARED(init_done, 1);
 }
 
@@ -238,6 +265,14 @@ error:
 	return ret;
 }
 
+static void fd_tracker_add(struct lttng_fd_entry *entry)
+{
+	struct cds_hlist_head *head;
+
+	head = fd_tracker_get_bucket(entry->fd);
+	cds_hlist_add_head(&entry->hlist, head);
+}
+
 /*
  * Needs to be called with ust_safe_guard_fd_mutex held when opening the fd.
  * Has strict checking of fd validity.
@@ -253,7 +288,11 @@ error:
  */
 int lttng_ust_add_fd_to_tracker(int fd)
 {
-	int ret;
+	struct lttng_fd_entry *entry = NULL;
+
+	if (caa_unlikely(fd < 0))
+		return -EBADF;
+
 	/*
 	 * Ensure the tracker is initialized when called from
 	 * constructors.
@@ -261,23 +300,32 @@ int lttng_ust_add_fd_to_tracker(int fd)
 	lttng_ust_fd_tracker_init();
 	assert(URCU_TLS(ust_fd_mutex_nest));
 
+	entry = zmalloc(sizeof(*entry));
+	if (!entry)
+		return -ENOMEM;
+
 	if (IS_FD_STD(fd)) {
-		ret = dup_std_fd(fd);
-		if (ret < 0) {
-			goto error;
+		fd = dup_std_fd(fd);
+		if (fd < 0) {
+			free(entry);
+			goto end;
 		}
-		fd = ret;
 	}
 
-	/* Trying to add an fd which we can not accommodate. */
-	assert(IS_FD_VALID(fd));
-	/* Setting an fd thats already set. */
-	assert(!IS_FD_SET(fd, lttng_fd_set));
+	/* Setting an fd that's already set. */
+	assert(!fd_tracker_is_fd_tracked(fd));
 
-	ADD_FD_TO_SET(fd, lttng_fd_set);
+	entry->fd = fd;
+
+	fd_tracker_add(entry);
+end:
 	return fd;
-error:
-	return ret;
+}
+
+static void fd_tracker_delete(struct lttng_fd_entry *entry)
+{
+	cds_hlist_del(&entry->hlist);
+	free(entry);
 }
 
 /*
@@ -286,6 +334,8 @@ error:
  */
 void lttng_ust_delete_fd_from_tracker(int fd)
 {
+	struct lttng_fd_entry *entry;
+
 	/*
 	 * Ensure the tracker is initialized when called from
 	 * constructors.
@@ -293,12 +343,11 @@ void lttng_ust_delete_fd_from_tracker(int fd)
 	lttng_ust_fd_tracker_init();
 
 	assert(URCU_TLS(ust_fd_mutex_nest));
-	/* Not a valid fd. */
-	assert(IS_FD_VALID(fd));
 	/* Deleting an fd which was not set. */
-	assert(IS_FD_SET(fd, lttng_fd_set));
+	entry = fd_tracker_lookup(fd);
+	assert(entry);
 
-	DEL_FD_FROM_SET(fd, lttng_fd_set);
+	fd_tracker_delete(entry);
 }
 
 /*
@@ -326,7 +375,7 @@ int lttng_ust_safe_close_fd(int fd, int (*close_cb)(int fd))
 		return close_cb(fd);
 
 	lttng_ust_lock_fd_tracker();
-	if (IS_FD_VALID(fd) && IS_FD_SET(fd, lttng_fd_set)) {
+	if (fd_tracker_is_fd_tracked(fd)) {
 		ret = -1;
 		errno = EBADF;
 	} else {
@@ -364,7 +413,7 @@ int lttng_ust_safe_fclose_stream(FILE *stream, int (*fclose_cb)(FILE *stream))
 	fd = fileno(stream);
 
 	lttng_ust_lock_fd_tracker();
-	if (IS_FD_VALID(fd) && IS_FD_SET(fd, lttng_fd_set)) {
+	if (fd_tracker_is_fd_tracked(fd)) {
 		ret = -1;
 		errno = EBADF;
 	} else {
@@ -399,7 +448,7 @@ static int test_close_success(const int *p __attribute__((unused)))
  */
 int lttng_ust_safe_closefrom_fd(int lowfd, int (*close_cb)(int fd))
 {
-	int ret = 0, close_success = 0, i;
+	int ret = 0, close_success = 0;
 
 	lttng_ust_fd_tracker_alloc_tls();
 
@@ -422,7 +471,7 @@ int lttng_ust_safe_closefrom_fd(int lowfd, int (*close_cb)(int fd))
 	 * validating whether the FD is part of the tracked set.
 	 */
 	if (URCU_TLS(ust_fd_mutex_nest)) {
-		for (i = lowfd; i < lttng_ust_max_fd; i++) {
+		for (unsigned int i = lowfd; i < lttng_ust_max_fd; i++) {
 			if (close_cb(i) < 0) {
 				switch (errno) {
 				case EBADF:
@@ -437,8 +486,8 @@ int lttng_ust_safe_closefrom_fd(int lowfd, int (*close_cb)(int fd))
 		}
 	} else {
 		lttng_ust_lock_fd_tracker();
-		for (i = lowfd; i < lttng_ust_max_fd; i++) {
-			if (IS_FD_VALID(i) && IS_FD_SET(i, lttng_fd_set))
+		for (unsigned int i = lowfd; i < lttng_ust_max_fd; i++) {
+			if (fd_tracker_is_fd_tracked(i))
 				continue;
 			if (close_cb(i) < 0) {
 				switch (errno) {
@@ -473,7 +522,7 @@ end:
 int lttng_ust_safe_close_range_fd(unsigned int first, unsigned int last, int flags,
 		int (*close_range_cb)(unsigned int first, unsigned int last, int flags))
 {
-	int ret = 0, i;
+	int ret = 0;
 
 	lttng_ust_fd_tracker_alloc_tls();
 
@@ -498,13 +547,13 @@ int lttng_ust_safe_close_range_fd(unsigned int first, unsigned int last, int fla
 			goto end;
 		}
 	} else {
-		int last_check = last;
+		unsigned int last_check = min_t(unsigned int,
+						last,
+						lttng_ust_max_fd);
 
-		if (last > lttng_ust_max_fd)
-			last_check = lttng_ust_max_fd;
 		lttng_ust_lock_fd_tracker();
-		for (i = first; i <= last_check; i++) {
-			if (IS_FD_VALID(i) && IS_FD_SET(i, lttng_fd_set))
+		for (unsigned int i = first; i <= last_check; i++) {
+			if (fd_tracker_is_fd_tracked(i))
 				continue;
 			if (close_range_cb(i, i, flags) < 0) {
 				ret = -1;
-- 
2.55.0

Reply via email to