[comp.os.minix] Request for help with rmdir bug

ast@botter.UUCP (06/03/87)

It has been previously reported that there is a bug in rmdir.  If you type

mkdir foo
rmdir foo/../foo

rmdir fails.  I checked the code of rmdir.  What happens is that rmdir
first unlinks . and .. from foo, then it tries to access foo/../foo to
unlink the directory, but that path is no longer valid because .. is now
gone.  I tried the same test on our VAX with 4.1 BSD and it failed
there too.  

The question is: does anyone have an idea how to make rmdir work without
modifying the kernel.  Unlinking the directory before removing . and ..
does not sound like a good idea.

Andy Tanenbaum (ast@cs.vu.nl)

mbd@basser.oz (David) (06/05/87)

In article <1195@botter.cs.vu.nl>, ast@cs.vu.nl (Andy Tanenbaum) writes:
> 
> It has been previously reported that there is a bug in rmdir.  If you type
> 
> mkdir foo
> rmdir foo/../foo
> 
> rmdir fails.  I checked the code of rmdir.  What happens is that rmdir
> first unlinks . and .. from foo, then it tries to access foo/../foo to
> unlink the directory, but that path is no longer valid because .. is now
> gone.  I tried the same test on our VAX with 4.1 BSD and it failed
> there too.  
It seems to work ok on our VAX running hacked up 32V, but I don't have
any way to check that the directory is not orphaned.
> 
I haven't tried this but:
	first cd to the directory specified by all but the last bit
	of the named directory ( i.e. if given foo/../foo then foo/.. )
	( This corresponds to what the 'dirname' command does ... )
	and then apply the existing algorithm to the last part.
Possible problem:
	Leaves you with a different current directory ( only a problem
	if you have multiple directories to remove. ).

The possible solution was suggested by Ralph Seberry.
Michael Barr-David (mbd@basser.oz.au)

polder@ark.UUCP (06/05/87)

In article 836 Andy Tanenbaum (ast@cs.vu.nl) writes:

> It has been previously reported that there is a bug in rmdir.  If you type
>
> mkdir foo
> rmdir foo/../foo
>
> rmdir fails.  I checked the code of rmdir.  What happens is that rmdir
> first unlinks . and .. from foo, then it tries to access foo/../foo to
> unlink the directory, but that path is no longer valid because .. is now
> gone. ....

Below is the source of a patched rmdir.
It is now immune to pathnames like 'foo/../foo', and
also checks to prevent things like 'rmdir foo/.'

--

Paul Polderman (polder@cs.vu.nl)

---CUT HERE---
/* rmdir - remove a directory		Author: Adri Koppes
 * (modified by Paul Polderman)
 */

#include "../include/signal.h"
#include "../include/stat.h"

struct direct {
    unsigned short  d_ino;
    char    d_name[14];
};
int     error = 0;

main (argc, argv)
register int argc;
register char  **argv;
{
    if (argc < 2) {
	prints ("Usage: rmdir dir ...\n");
	exit (1);
    }
    signal (SIGHUP, SIG_IGN);
    signal (SIGINT, SIG_IGN);
    signal (SIGQUIT, SIG_IGN);
    signal (SIGTERM, SIG_IGN);
    while (--argc)
	remove (*++argv);
    if (error)
	exit (1);
}

extern char *rindex();

remove (dirname)
char   *dirname;
{
    struct direct   d;
    struct stat s,
                cwd;
    register int fd = 0, sl = 0;
    char    dots[128];
    register char *p;

    if (stat (dirname, &s)) {
	stderr2(dirname, " doesn't exist\n");
	error++;
	return;
    }
    if ((s.st_mode & S_IFMT) != S_IFDIR) {
	stderr2(dirname, " not a directory\n");
	error++;
	return;
    }
    if (p = rindex(dirname, '/'))
	p++;
    else
	p = dirname;

    if (strcmp(p, ".") == 0 || strcmp(p, "..") == 0) {
	stderr2(dirname, " will not remove \".\" or \"..\"\n");
	error++;
	return;
    }

    strcpy (dots, dirname);
    while (dirname[fd])
	if (dirname[fd++] == '/')
	    sl = fd;
    dots[sl] = '\0';
    if (access (dots, 2)) {
	stderr2(dirname, " no permission\n");
	error++;
	return;
    }
    stat ("", &cwd);
    if ((s.st_ino == cwd.st_ino) && (s.st_dev == cwd.st_dev)) {
	std_err ("rmdir: can't remove current directory\n");
	error++;
	return;
    }
    if ((fd = open (dirname, 0)) < 0) {
	stderr2("can't read ", dirname);
	std_err("\n");
	error++;
	return;
    }
    while (read (fd, (char *) & d, sizeof (struct direct)) == sizeof (struct direct))
    if (d.d_ino != 0)
	if (strcmp (d.d_name, ".") && strcmp (d.d_name, "..")) {
            stderr2(dirname, " not empty\n");
	    close(fd);
	    error++;
	    return;
	}
    close (fd);
    strcpy (dots, dirname);
    strcat (dots, "/..");
    patch_path(dots);
    for (p = dots; *p; p++)	/* find end of dots */
	;
    unlink(dots);		/* dirname/.. */
    *(p - 1) = '\0';
    unlink(dots);		/* dirname/. */
    *(p - 3) = '\0';
    if (unlink(dots)) {		/* dirname */
	stderr2("can't remove ", dots);
	std_err("\n");
	error++;
	return;
    }
}

stderr2(s1, s2)
char *s1, *s2;
{
	std_err("rmdir: ");
	std_err(s1);
	std_err(s2);
}

patch_path(dir)
char *dir;	/* pathname ending with "/.." */
{
	register char *p, *s;
	struct stat pst, st;

	if (stat(dir, &pst) < 0)
		return;
	p = dir;
	while (*p == '/') p++;
	while (1) {
		s = p;		/* remember start of new pathname part */
		while (*p && *p != '/')
			p++;	/* find next slash */
		if (*p == '\0')
			return;	/* if end of pathname, return */

		/* check if this part of pathname == the original pathname */
		*p = '\0';
		stat(dir, &st);
		if (st.st_ino == pst.st_ino && st.st_dev == pst.st_dev
			&& strcmp(s, "..") == 0)
				return;	
		/* if not, try next part */
		*p++ = '/';
		while (*p == '/') p++;
	}
}

reid@sask.UUCP (06/08/87)

In article <1195@botter.cs.vu.nl> ast@cs.vu.nl (Andy Tanenbaum) writes:
> ...
>rmdir fails.  I checked the code of rmdir.  What happens is that rmdir
>first unlinks . and .. from foo, then it tries to access foo/../foo to
>unlink the directory, but that path is no longer valid because .. is now
>gone.  I tried the same test on our VAX with 4.1 BSD and it failed
>there too.  
>
>The question is: does anyone have an idea how to make rmdir work without
>modifying the kernel.  Unlinking the directory before removing . and ..
>does not sound like a good idea.
>
>Andy Tanenbaum (ast@cs.vu.nl)


I have no idea how any Unices do it, but how about:

/* rmdir.pseudo-c: */

rmdir(directory)
{
	chdir("directory/..");
	dname <- name of directory with any path stripped off;
	unlink(dname/..);
	unlink(dname/.);
	unlink(dname);
}

This would fail at the root directory, because /. and /.. are both links
to / (at least on the Unices I've used), but should work everywhere else.

(by the way, Ultrix 1.2 (almost 4.2BSD) handles the bad rmdir correctly)

-- 
 - !Baboo -   (reid@sask.uucp or {alberta, ihnp4, utcsri}!sask!reid)

Rabbi, is there a proper blessing for the Tsar?
Certainly, my son.  May God bless and keep the Tsar...  Far away from us.

henry@utzoo.UUCP (Henry Spencer) (06/08/87)

> The question is: does anyone have an idea how to make rmdir work without
> modifying the kernel.  Unlinking the directory before removing . and ..
> does not sound like a good idea.

This is a known bug in old Unixes.  The simplest approach is to have rmdir
disallow removals of directories with pathnames involving "." or "..".

The fundamental problem is that the removal of the three links is not an
atomic operation.  Even if one fixes rmdir to be proof against tricky
pathnames, more subtle problems can arise because in a multiprogramming
system, other things can be manipulating the directories at the same time.
The only complete fix for race conditions and such is to move the mkdir
and rmdir operations into the kernel.
-- 
"There is only one spacefaring        Henry Spencer @ U of Toronto Zoology
nation on Earth today, comrade."   {allegra,ihnp4,decvax,pyramid}!utzoo!henry