Subject: bin/19377: In ftpd, pwd will fail in a directory with mode 111.
To: None <gnats-bugs@gnats.netbsd.org>
From: None <manu@netbsd.org>
List: netbsd-bugs
Date: 12/13/2002 19:07:22
>Number:         19377
>Category:       bin
>Synopsis:       In ftpd, pwd will fail in a directory with mode 111.
>Confidential:   no
>Severity:       non-critical
>Priority:       medium
>Responsible:    bin-bug-people
>State:          open
>Class:          sw-bug
>Submitter-Id:   net
>Arrival-Date:   Fri Dec 13 10:08:01 PST 2002
>Closed-Date:
>Last-Modified:
>Originator:     Emmanuel Dreyfus
>Release:        NetBSD 1.6
>Organization:
NetBSD
>Environment:
System: NetBSD melancolie 1.6 NetBSD 1.6 (GENERIC) #0: Sun Sep 8 19:43:40 UTC 2002 autobuild@tgm.daemon.org:/autobuild/i386/OBJ/autobuild/src/sys/arch/i386/compile/GENERIC i386
Architecture: i386
Machine: i386
>Description:
	When the user go through a mode 111 directory, there are situations
	where ftpd is unable to answer to the pwd request. This confuses 
	some FTP clients.

	This is annoying since other FTP servers, such as WU-ftpd, are able
	to answer to pwd in this kind of situations.

>How-To-Repeat:
	# mkdir /ftp/pub/shadow
	# chmod 111 /ftp/pub/shadow
	# mkdir /ftp/pub/shadow/test
	# chod 755 /ftp/pub/shadow/test
	# ftp ftp://localhost/pub/shadow/test/
	(blah blah blah)
	ftp> pwd
	550 Can't get the current directory: Permission denied.

>Fix:
	The problem is that we retreive the path with getcwd(), and that 
	getcwd needs the +x permission on each directory in the path.

	We can keep track of where the user went, and when we are asked about
	pwd, return this cached pwd instead of retreiving it if we are unable
	to do so. This is what WU-ftpd does. 

	I attached  a patch that does an hybrid between cached path and 
	getcwd(): if getcwd works, use it, else use a cached path.

	I tested it, it works well, except that it has trouble to follow
	symlinks correctly when the link crosses mode 111 directories. The 
	displayed path is still correct, but it goes through the link instead 
	of going through the target. Example with the preceding situation:
	# mkdir /ftp/pub/test
	# ln -sf ../shadow/test /ftp/pub/test/a
	ftp ftp://localhost/pub/test/
	(blah blah)
	ftp> cd a
	250 CWD command successful.
	ftp> pwd
	257 "/pub/test/b" is the current directory.

	While this fix is good enough to prevent some client from getting
	crazy, we may consider a more general fix at the libc level: the
	same problem exists in /bin/sh: once in a mode 111 directory, pwd
	does not work anymore. /bin/ksh seems to keep track of the path 
	itself and it is able to display the directory.

Index: cmds.c
===================================================================
RCS file: /cvsroot/basesrc/libexec/ftpd/cmds.c,v
retrieving revision 1.16.2.1
diff -U4 -r1.16.2.1 cmds.c
--- cmds.c	2002/11/01 08:23:39	1.16.2.1
+++ cmds.c	2002/12/13 17:38:47
@@ -170,8 +170,9 @@
 };
 
 #define FACTTABSIZE	(sizeof(facttab) / sizeof(struct ftpfact))
 
+static char cached_path[MAXPATHLEN + 1] = "/";
 
 void
 cwd(const char *path)
 {
@@ -180,8 +181,48 @@
 		perror_reply(550, path);
 	else {
 		show_chdir_messages(250);
 		ack("CWD");
+		if (getcwd(cached_path, MAXPATHLEN) == NULL) {
+			char tmp_path[MAXPATHLEN + 1] = "";
+			char *cp;
+		 	char *cq;
+			
+			if (path[0] != '/') {
+				(void)strncpy(tmp_path, 
+				    cached_path, MAXPATHLEN);
+				(void)strncat(tmp_path, "/", MAXPATHLEN);
+			}
+			(void)strncat(tmp_path, path, MAXPATHLEN);
+			(void)strncat(tmp_path, "/", MAXPATHLEN);
+			
+			/* Collapse any // into / */
+			while ((cp = strstr(tmp_path, "//")) != NULL)
+				(void)memmove(cp, cp + 1, strlen(cp) - 1 + 1);
+
+			/* Collapse any /./ into / */
+			while ((cp = strstr(tmp_path, "/./")) != NULL)
+				(void)memmove(cp, cp + 2, strlen(cp) - 2 + 1);
+
+			/* Collapse any /foo/../ into /foo/ */
+			while ((cp = strstr(tmp_path, "/../")) != NULL) {
+				/* ^/../foo/ becomes ^/foo/ */
+				if (cp == tmp_path) {
+					(void)memmove(cp, cp + 3, 
+					    strlen(cp) - 3 + 1);
+				} else {
+					for (cq = cp - 1; *cq != '/'; cq--); 
+					(void)memmove(cq, cp + 3,
+				 	    strlen(cp) - 3 + 1);
+				}
+			}
+
+			/* Strip trailing / if path is not just ^/$ */
+			if (strlen(tmp_path) != 1)
+				tmp_path[strlen(tmp_path) - 1] = '\0';
+
+			(void)strncpy(cached_path, tmp_path, MAXPATHLEN);
+		}
 	}
 }
 
 void
@@ -403,13 +444,11 @@
 pwd(void)
 {
 	char path[MAXPATHLEN];
 
-	if (getcwd(path, sizeof(path) - 1) == NULL)
-		reply(550, "Can't get the current directory: %s.",
-		    strerror(errno));
-	else
-		replydirname(path, "is the current directory.");
+	if (getcwd(path, sizeof(path) - 1) == NULL) 
+		(void)strncpy(path, cached_path, MAXPATHLEN);
+	replydirname(path, "is the current directory.");
 }
 
 void
 removedir(const char *name)
>Release-Note:
>Audit-Trail:
>Unformatted: