[comp.os.minix] Nasty bug in getcwd and proposed fix

ast@cs.vu.nl (Andy Tanenbaum) (02/10/90)

One of my students, Jan-Mark Wams, wrote a test program that discovered
an extremely nasty bug in getcwd().  The way getcwd works is it works its
way up the directory tree, doing chdir and stat as it goes.  When it gets
to the root, it does a chdir back to the starting directory.  

However, if it fails to get all the way back to the root, it just returns,
leaving the caller in some intermediate directory.

I hacked it a little bit, and I think it is better now, but I am not really
confident that I got it right in all cases.  Comments anyone (via news)?

Andy Tanenbaum (ast@cs.vu.nl)


--------------------- new getcwd.c -------------------
/*  getcwd - get current working directory	Author: Terrence W. Holm */

/* Directly derived from Adri Koppes' pwd(1).
 * Modified by Andy Tanenbaum for POSIX (29 Oct. 1989)
 */

#include <lib.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/dir.h>
#include <unistd.h>
#include <string.h>

#define  DIRECT_SIZE  (sizeof (struct direct))

extern char *rindex();

char *getcwd(buffer, size)
char *buffer;
int size;
/* Get current working directory. */
{
  int same_device, found, fd;
  char *r, path[PATH_MAX + 1], temp_name[NAME_MAX + 1];
  struct stat current, parent, dir_entry;
  struct direct d;

  if (buffer == (char *)NULL || size <= 0) {
	errno = EINVAL;
	return((char *)NULL);
  }
  path[0] = '\0';

  /* Get the inode for the current directory  */
  if (stat(".", &current) == -1) return((char *)NULL);
  if ((current.st_mode & S_IFMT) != S_IFDIR) return((char *)NULL);

  /* Run backwards up the directory tree, grabbing dir names on the way. */
  while (1) {
	same_device = 0;
	found = 0;

	/* Get the inode for the parent directory  */
	if (chdir("..") == -1) return((char *)NULL);
	if (stat(".", &parent) == -1) return((char *)NULL);
	if ((parent.st_mode & S_IFMT) != S_IFDIR) return((char *)NULL);
	if (current.st_dev == parent.st_dev) same_device = 1;

	/* At the root, "." is the same as ".."  */
	if (same_device && current.st_ino == parent.st_ino) break;

	/* Search the parent directory for the current entry  */
	if ((fd = open(".", O_RDONLY)) == -1) return((char *)NULL);
	while (!found && read(fd, (char *)&d, DIRECT_SIZE) == DIRECT_SIZE) {
		if (same_device) {
			if (current.st_ino == d.d_ino) found = 1;
		} else {
			temp_name[0] = '\0';
			strncat(temp_name, d.d_name, NAME_MAX);
			if (stat(temp_name, &dir_entry) == -1) {
				close(fd);
				go_back(path);
				return((char *)NULL);
			}
			if (current.st_dev == dir_entry.st_dev &&
			    current.st_ino == dir_entry.st_ino)
				found = 1;
		}
	}

	close(fd);
	if (!found) {
		go_back(path);
		return((char *)NULL);
	}
	if (strlen(path) + NAME_MAX + 1 > PATH_MAX) {
		errno = ERANGE;
		go_back(path);
		return((char *)NULL);
	}
	strcat(path, "/");
	strncat(path, d.d_name, NAME_MAX);
	current.st_dev = parent.st_dev;
	current.st_ino = parent.st_ino;
  }

  /* Copy the reversed path name into <buffer>  */
  if (strlen(path) + 1 > size) {
	errno = ERANGE;
	go_back(path);
	return((char *)NULL);
  }
  if (strlen(path) == 0) {
	strcpy(buffer, "/");
	return(buffer);
  }
  *buffer = '\0';
  while ((r = rindex(path, '/')) != (char *)NULL) {
	strcat(buffer, r);
	*r = '\0';
  }
  return(chdir(buffer) ? (char *)NULL : buffer);
}

PRIVATE go_back(path)
char *path;
{
/* If getcwd() gets in trouble and can't complete normally, reverse the
 * path built so far and change there so we end up in the directory that
 * we started in.
 */

  char *r;

  while ((r = rindex(path, '/')) != (char *)NULL) {
	chdir(r+1);
	*r = '\0';
  }
}