Follow @Openwall on Twitter for new release announcements and other news
[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-ID: <20250721002322.703-3-mailto.luca.kellermann@gmail.com>
Date: Mon, 21 Jul 2025 02:23:21 +0200
From: Luca Kellermann <mailto.luca.kellermann@...il.com>
To: Rich Felker <dalias@...c.org>
Cc: Markus Wichmann <nullplan@....net>,
	musl@...ts.openwall.com
Subject: [PATCH v2 3/4] scandir: disable cancellation around cancellation points

opendir() or closedir() might act upon a cancellation request. because
scandir() did not disable cancellation or 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 | 16 ++++++++++++++--
 1 file changed, 14 insertions(+), 2 deletions(-)

diff --git a/src/dirent/scandir.c b/src/dirent/scandir.c
index cf668535..4f91af9c 100644
--- a/src/dirent/scandir.c
+++ b/src/dirent/scandir.c
@@ -1,4 +1,5 @@
 #include <dirent.h>
+#include <pthread.h>
 #include <string.h>
 #include <stdlib.h>
 #include <stdint.h>
@@ -9,11 +10,20 @@ 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);
+	DIR *d;
 	struct dirent *de, **names=0, **tmp;
 	size_t cnt=0, len=0;
-	int old_errno = errno, err;
+	int old_errno = errno, err, cs;
 
+	/* opendir() and closedir() are cancellation points. scandir() is also
+	 * allowed to be a cancellation point but we choose not to make it one.
+	 * To avoid calling sel() and cmp() with altered thread state,
+	 * cancellation is not explicitly disabled for those calls. This means
+	 * if either of the callbacks acts upon a cancellation request, there
+	 * can be memory and file descriptor leaks. */
+	pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &cs);
+	d = opendir(path);
+	pthread_setcancelstate(cs, 0);
 	if (!d) return -1;
 
 	while ((errno=0), (de = readdir(d))) {
@@ -39,7 +49,9 @@ int scandir(const char *path, struct dirent ***res,
 	 * "failure" of closedir() as a failure of scandir(). */
 	err = errno;
 
+	pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &cs);
 	closedir(d);
+	pthread_setcancelstate(cs, 0);
 
 	if (err) {
 		if (names) while (cnt-->0) free(names[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.