Subject: Re[2]: popen reentrant (was Re: SA/pthread and vfork)
To: Nathan J. Williams <nathanw@wasabisystems.com>
From: Christian Limpach <chris@pin.lu>
List: tech-kern
Date: 09/13/2003 00:10:05
--4605703-10461-1063404607=:2576
Content-Type: TEXT/PLAIN; CHARSET=US-ASCII
Content-Disposition: INLINE

On 12 Sep 2003 16:41:58 -0400 "Nathan J. Williams" <nathanw@wasabisystems.com> wrote:
Nathan J. Williams <nathanw@wasabisystems.com> writes:
> Christian Limpach <chris@pin.lu> writes:
> 
> > The appended patch should make popen()/pclose() reentrant.  It adds a
> > lockfile for access to the pidlist and it prevents concurrent pclose/fclose
> > calls on the descriptors in the pidlist from interfering with the child
> > closing those descriptors.
> 
> (Man, I had no idea that popen()/pclose() went through all these
> contortions. Yuck.)

yes :-(

> Why is this using a rwlock instead of a mutex, since it never uses the
> lock in read-only mode?

I was using the lock in read-only mode at first before I added the caching
of file descriptors.

> What's the reason for caching the file descriptor numbers of all pipes
> in the pidlist? It's okay to use fileno(); FLOCKFILE()/FUNLOCKFILE()
> are recursive locks.

well, the problem is that the child is going to lose if it calls fileno()
on a file descriptor which is locked in the parent.  The child's
fileno/FLOCKFILE call will call cond_wait() and this will (try to) switch
to a different thread.  I've observed this exact failure case.

Appended is a new patch which is simpler:
- caches the file descriptor at creation time.
- doesn't hold the locks on the file streams during the vfork.

This is more like what I had at first, before I got overzealous and thought
it would be necessary to prevent an fclose() on a FILE* returned by popen()
from interfering with the child's closing of the same file handles.  This
is not necessary since the only close operation supported on FILE* returned
by popen() is pclose() and this is handled by the pidlist lock.

I have left the file descriptor caching conditional on _REENTRANT, it saves
a few bytes in the !_REENTRANT case.  I don't know if it's worth it, but
making it conditional is conservative.

Thank you for your comments.


--4605703-10461-1063404607=:2576
Content-Type: TEXT/PLAIN; CHARSET=US-ASCII; NAME="popen-reentrant2.txt"
Content-Disposition: INLINE; FILENAME="popen-reentrant2.txt"

Index: lib/libc/gen/popen.c
===================================================================
RCS file: /cvs/netbsd/src/lib/libc/gen/popen.c,v
retrieving revision 1.27
diff -u -r1.27 popen.c
--- lib/libc/gen/popen.c	7 Aug 2003 16:42:55 -0000	1.27
+++ lib/libc/gen/popen.c	12 Sep 2003 22:06:57 -0000
@@ -68,9 +68,16 @@
 static struct pid {
 	struct pid *next;
 	FILE *fp;
+#ifdef _REENTRANT
+	int fd;
+#endif
 	pid_t pid;
 } *pidlist; 
 	
+#ifdef _REENTRANT
+static rwlock_t pidlist_lock = RWLOCK_INITIALIZER;
+#endif
+
 FILE *
 popen(command, type)
 	const char *command, *type;
@@ -108,11 +115,13 @@
 		return (NULL);
 	}
 
+	rwlock_rdlock(&pidlist_lock);
 	rwlock_rdlock(&__environ_lock);
 	switch (pid = vfork()) {
 	case -1:			/* Error. */
 		serrno = errno;
 		rwlock_unlock(&__environ_lock);
+		rwlock_unlock(&pidlist_lock);
 		free(cur);
 		(void)close(pdes[0]);
 		(void)close(pdes[1]);
@@ -124,7 +133,11 @@
 		   from previous popen() calls that remain open in the 
 		   parent process are closed in the new child process. */
 		for (old = pidlist; old; old = old->next)
+#ifdef _REENTRANT
+			close(old->fd); /* don't allow a flush */
+#else
 			close(fileno(old->fp)); /* don't allow a flush */
+#endif
 
 		if (*type == 'r') {
 			(void)close(pdes[0]);
@@ -151,9 +164,15 @@
 	/* Parent; assume fdopen can't fail. */
 	if (*type == 'r') {
 		iop = fdopen(pdes[0], type);
+#ifdef _REENTRANT
+		cur->fd = pdes[0];
+#endif
 		(void)close(pdes[1]);
 	} else {
 		iop = fdopen(pdes[1], type);
+#ifdef _REENTRANT
+		cur->fd = pdes[1];
+#endif
 		(void)close(pdes[0]);
 	}
 
@@ -162,6 +181,7 @@
 	cur->pid =  pid;
 	cur->next = pidlist;
 	pidlist = cur;
+	rwlock_unlock(&pidlist_lock);
 
 	return (iop);
 }
@@ -181,25 +201,32 @@
 
 	_DIAGASSERT(iop != NULL);
 
+	rwlock_wrlock(&pidlist_lock);
+
 	/* Find the appropriate file pointer. */
 	for (last = NULL, cur = pidlist; cur; last = cur, cur = cur->next)
 		if (cur->fp == iop)
 			break;
-	if (cur == NULL)
+	if (cur == NULL) {
+		rwlock_unlock(&pidlist_lock);
 		return (-1);
+	}
 
 	(void)fclose(iop);
 
-	do {
-		pid = waitpid(cur->pid, &pstat, 0);
-	} while (pid == -1 && errno == EINTR);
-
 	/* Remove the entry from the linked list. */
 	if (last == NULL)
 		pidlist = cur->next;
 	else
 		last->next = cur->next;
+
+	rwlock_unlock(&pidlist_lock);
+
+	do {
+		pid = waitpid(cur->pid, &pstat, 0);
+	} while (pid == -1 && errno == EINTR);
+
 	free(cur);
-		
+
 	return (pid == -1 ? -1 : pstat);
 }

--4605703-10461-1063404607=:2576
Content-Type: TEXT/PLAIN; CHARSET=US-ASCII
Content-Disposition: INLINE


-- 
Christian Limpach <chris@pin.lu>

--4605703-10461-1063404607=:2576--