Follow @Openwall on Twitter for new release announcements and other news
[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-ID: <20250710185131.186-3-mailto.luca.kellermann@gmail.com>
Date: Thu, 10 Jul 2025 20:51:30 +0200
From: Luca Kellermann <mailto.luca.kellermann@...il.com>
To: musl@...ts.openwall.com
Subject: [PATCH 3/4] scandir: fix leaks caused by cancellation

opendir(), sel(), closedir(), or cmp() might act upon a cancellation
request. because scandir() did not install a cancellation cleanup
handler, this could lead to memory and file descriptor leaks.

the following program demonstrates the leaks:

#include <dirent.h>
#include <errno.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
static void die(const char *msg)
{
	int cs, e = errno;
	if (!pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &cs)) {
		errno = e;
		perror(msg);
	}
	_Exit(EXIT_FAILURE);
}
static int posted;
static sem_t sem;
static int sel(const struct dirent *e)
{
	if (!posted && sem_post(&sem)) die("sem_post");
	posted = 1;
	return 1;
}
static void *run(void *arg)
{
	posted = 0;
	struct dirent **es = 0;
	int n = scandir(".", &es, sel, alphasort);
	if (n == -1) die("scandir");
	while (n > 0) free(es[--n]);
	free(es);
	return 0;
}
int main(void)
{
	if (sem_init(&sem, 0, 0)) die("sem_init");
	void *res = 0;
	do {
		pthread_t tid;
		if (errno = pthread_create(&tid, 0, run, 0))
			die("pthread_create");
		if (sem_wait(&sem)) die("sem_wait");
		if (errno = pthread_cancel(tid))
			die("pthread_cancel");
		if (errno = pthread_join(tid, &res))
			die("pthread_join");
	} while (res != PTHREAD_CANCELED);
	if (sem_destroy(&sem)) die("sem_destroy");
}

$ valgrind -q --leak-check=full --show-leak-kinds=all ./scandir-leak
==31293== 296 bytes in 9 blocks are indirectly lost in loss record 1 of 3
==31293==    at 0x48AF828: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==31293==    by 0x401C500: scandir (scandir.c:32)
==31293==    by 0x10933F: run (scandir-leak.c:28)
==31293==    by 0x405E653: start (pthread_create.c:207)
==31293==    by 0x4060DEA: ??? (clone.s:22)
==31293==
==31293== 416 (120 direct, 296 indirect) bytes in 1 blocks are definitely lost in loss record 2 of 3
==31293==    at 0x48B6B80: realloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==31293==    by 0x401C4EA: scandir (scandir.c:28)
==31293==    by 0x10933F: run (scandir-leak.c:28)
==31293==    by 0x405E653: start (pthread_create.c:207)
==31293==    by 0x4060DEA: ??? (clone.s:22)
==31293==
==31293== 2,072 bytes in 1 blocks are definitely lost in loss record 3 of 3
==31293==    at 0x48B6953: calloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==31293==    by 0x401C2E0: opendir (opendir.c:15)
==31293==    by 0x401C491: scandir (scandir.c:12)
==31293==    by 0x10933F: run (scandir-leak.c:28)
==31293==    by 0x405E653: start (pthread_create.c:207)
==31293==    by 0x4060DEA: ??? (clone.s:22)
==31293==

$ strace -fe t=open,close ./scandir-leak
strace: Process 27136 attached
[pid 27136] open(".", O_RDONLY|O_LARGEFILE|O_CLOEXEC|O_DIRECTORY) = 3
[pid 27136] --- SIGRT_1 {si_signo=SIGRT_1, si_code=SI_TKILL, si_pid=27135, si_uid=1000} ---
[pid 27136] +++ exited with 0 +++
+++ exited with 0 +++
---
 src/dirent/scandir.c | 75 ++++++++++++++++++++++++++++++--------------
 1 file changed, 52 insertions(+), 23 deletions(-)

diff --git a/src/dirent/scandir.c b/src/dirent/scandir.c
index cf668535..8883b9f8 100644
--- a/src/dirent/scandir.c
+++ b/src/dirent/scandir.c
@@ -1,37 +1,61 @@
 #include <dirent.h>
+#include <pthread.h>
 #include <string.h>
 #include <stdlib.h>
 #include <stdint.h>
 #include <errno.h>
 #include <stddef.h>
 
+struct cleanup_params {
+	DIR *d;
+	struct dirent **names;
+	size_t cnt;
+};
+
+static void cleanup(void *p)
+{
+	struct cleanup_params *c = p;
+	/* Cancellation is disabled if c->d is not NULL, so closedir()
+	 * won't act upon a cancellation request here. */
+	if (c->d) closedir(c->d);
+	if (c->names) {
+		size_t cnt = c->cnt;
+		while (cnt-->0) free(c->names[cnt]);
+		free(c->names);
+	}
+}
+
 int scandir(const char *path, struct dirent ***res,
 	int (*sel)(const struct dirent *),
 	int (*cmp)(const struct dirent **, const struct dirent **))
 {
-	DIR *d = opendir(path);
-	struct dirent *de, **names=0, **tmp;
-	size_t cnt=0, len=0;
+	/* opendir() might act upon a cancellation request. */
+	struct cleanup_params c = {.d = opendir(path)};
+	struct dirent *de, **tmp;
+	size_t len=0;
 	int old_errno = errno, err;
 
-	if (!d) return -1;
+	if (!c.d) return -1;
+
+	/* sel(), closedir(), or cmp() might act upon a cancellation request. */
+	pthread_cleanup_push(cleanup, &c);
 
-	while ((errno=0), (de = readdir(d))) {
+	while ((errno=0), (de = readdir(c.d))) {
 		if (sel) {
 			/* sel() must not observe that errno was set to 0. */
 			errno = old_errno;
 			if (!sel(de)) continue;
 		}
-		if (cnt >= len) {
+		if (c.cnt >= len) {
 			len = 2*len+1;
-			if (len > SIZE_MAX/sizeof *names) break;
-			tmp = realloc(names, len * sizeof *names);
+			if (len > SIZE_MAX/sizeof *c.names) break;
+			tmp = realloc(c.names, len * sizeof *c.names);
 			if (!tmp) break;
-			names = tmp;
+			c.names = tmp;
 		}
-		names[cnt] = malloc(de->d_reclen);
-		if (!names[cnt]) break;
-		memcpy(names[cnt++], de, de->d_reclen);
+		c.names[c.cnt] = malloc(de->d_reclen);
+		if (!c.names[c.cnt]) break;
+		memcpy(c.names[c.cnt++], de, de->d_reclen);
 	}
 	/* closedir() might set errno via __aio_close(). It might also "fail"
 	 * (return -1 and set errno). But even then, the file descriptor is
@@ -39,18 +63,23 @@ int scandir(const char *path, struct dirent ***res,
 	 * "failure" of closedir() as a failure of scandir(). */
 	err = errno;
 
-	closedir(d);
+	/* If closedir() acts upon a cancellation request, it is retried by
+	 * cleanup() with cancellation disabled. closedir() won't act upon a
+	 * cancellation request in masked cancellation mode, meaning errno does
+	 * not need to be inspected for ECANCELED. */
+	closedir(c.d);
+	/* cleanup() shouldn't call closedir() from here on, because the
+	 * directory stream is already closed. */
+	c.d = 0;
 
-	if (err) {
-		if (names) while (cnt-->0) free(names[cnt]);
-		free(names);
-		errno = err;
-		return -1;
+	if (!err) {
+		/* cmp() and caller must not observe that errno was set to 0. */
+		errno = old_errno;
+		if (cmp) qsort(c.names, c.cnt, sizeof *c.names, (int (*)(const void *, const void *))cmp);
+		*res = c.names;
 	}
-	/* cmp() and caller must not observe that errno was set to 0. */
-	errno = old_errno;
 
-	if (cmp) qsort(names, cnt, sizeof *names, (int (*)(const void *, const void *))cmp);
-	*res = names;
-	return cnt;
+	pthread_cleanup_pop(err);
+
+	return err ? (errno = err), -1 : c.cnt;
 }
-- 
2.43.0

Powered by blists - more mailing lists

Confused about mailing lists and their use? Read about mailing lists on Wikipedia and check out these guidelines on proper formatting of your messages.