Skip to contentSkip to conversation

libfs dcache_readdir() and dcache_dir_lseek() bugfix

RFC PATCH In review [email protected] 6 participants 23 messages Nov 4, 2005 – Nov 7, 2005 VFS

lore
Share & tools
lore
#0 lore raw

RFC PATCH 1/1 libfs dcache_readdir() and dcache_dir_lseek() bugfix

This patch effects all users of libfs' dcache directory implementation.

Old glibc implementations (e.g. glibc-2.2.5) are lseeking after every call to getdents(), subsequent calls to getdents() are starting to read from a wrong f_pos, when the directory is modified in between. Therefore not all directory entries are returned. IMHO this is a bug and it breaks applications, e.g. "rm -fr" on tmpfs.

SuSV3 only says:
"If a file is removed from or added to the directory after the most recent call to opendir() or rewinddir(), whether a subsequent call to readdir_r() returns an entry for that file is unspecified."

Although I tried to provide a small fix, this is a complete rework of the libfs' dcache_readdir() and dcache_dir_lseek(). This patch use the dentries name hashes as the offsets for the directory entries. I'm not that sure it is maybe a problem to use full_name_hash() for that. If hash collisions occur, readdir() might return duplicate entries after an lseek(). Any ideas?

This patch is compile and boot tested. This patch is against 2.6.14-git of today.

Comments, please.
Jan

Signature · 4 lines
Jan Blunck                                               [email protected]
SuSE LINUX AG - A Novell company
Maxfeldstr. 5                                          +49-911-74053-608
D-90409 Nürnberg                                      http://www.suse.de

#include <stdio.h>
#include <unistd.h>
#include <dirent.h>

int main(int argc, char *argv[])

{
	DIR *dir;
	struct dirent *entry;
	unsigned int offset;

        if (argc < 2) {
                printf("USAGE:  %s <directory>\n", argv[0]);
                return 1;
        }

	dir = opendir(argv[1]);
	if (!dir)
		return 1;

	while((entry = readdir(dir)) != NULL) {
		seekdir(dir, entry->d_off);
		printf("name=%s\tino=%d off=%d\n", entry->d_name, entry->d_ino,
		       entry->d_off);
		if (*entry->d_name == '.')
			continue;
		if(unlink(entry->d_name) != 0)
			break;
	}

	closedir(dir);
	return 0;
}
 fs/libfs.c |  140 +++++++++++++++++++++++++++++++++----------------------------
 1 files changed, 76 insertions(+), 64 deletions(-)
fs/libfs.c +76 -64
Index: linux-2.6/fs/libfs.c
===================================================================
@@ -72,42 +72,57 @@ int dcache_dir_close(struct inode *inode
 	return 0;
 }
 
-loff_t dcache_dir_lseek(struct file *file, loff_t offset, int origin)
+loff_t dcache_dir_lseek(struct file * file, loff_t offset, int origin)
 {
+	struct dentry * dentry = file->f_dentry;
+	int found = 0;
+
+	/* for directories it doesn't make any sense to seek relative */
+	if (origin != 0)
+		return -EINVAL;
+
 	down(&file->f_dentry->d_inode->i_sem);
-	switch (origin) {
-		case 1:
-			offset += file->f_pos;
-		case 0:
-			if (offset >= 0)
-				break;
-		default:
-			up(&file->f_dentry->d_inode->i_sem);
-			return -EINVAL;
-	}
 	if (offset != file->f_pos) {
-		file->f_pos = offset;
-		if (file->f_pos >= 2) {
-			struct list_head *p;
-			struct dentry *cursor = file->private_data;
-			loff_t n = file->f_pos - 2;
+		struct list_head *p;
+		struct dentry *cursor = file->private_data;
 
+		// Handle the special values
+		if ((offset == 0)
+		    || (offset == full_name_hash(".", 1))
+		    || (offset == full_name_hash("..", 2))) {
+			found = 1;
+			file->f_pos = offset;
 			spin_lock(&dcache_lock);
 			list_del(&cursor->d_child);
-			p = file->f_dentry->d_subdirs.next;
-			while (n && p != &file->f_dentry->d_subdirs) {
-				struct dentry *next;
-				next = list_entry(p, struct dentry, d_child);
-				if (!d_unhashed(next) && next->d_inode)
-					n--;
-				p = p->next;
+			list_add(&cursor->d_child, &dentry->d_subdirs);
+			spin_unlock(&dcache_lock);
+			goto out;
+		}
+
+		spin_lock(&dcache_lock);
+		list_del(&cursor->d_child);
+		p = file->f_dentry->d_subdirs.next;
+		while (p != &file->f_dentry->d_subdirs) {
+			struct dentry *next;
+			next = list_entry(p, struct dentry, d_child);
+			if (!d_unhashed(next) && next->d_inode
+			    && (offset == full_name_hash(next->d_name.name,
+							 next->d_name.len))) {
+				found = 1;
+				break;
 			}
+			p = p->next;
+		}
+
+		if (found) {
 			list_add_tail(&cursor->d_child, p);
-			spin_unlock(&dcache_lock);
+			file->f_pos = offset;
 		}
+		spin_unlock(&dcache_lock);
 	}
+out:
 	up(&file->f_dentry->d_inode->i_sem);
-	return offset;
+	return (found ? offset : -EINVAL);
 }
 
 /* Relationship between i_mode and the DT_xxx types */
@@ -128,47 +143,44 @@ int dcache_readdir(struct file * filp, v
 	struct dentry *cursor = filp->private_data;
 	struct list_head *p, *q = &cursor->d_child;
 	ino_t ino;
-	int i = filp->f_pos;
 
-	switch (i) {
-		case 0:
-			ino = dentry->d_inode->i_ino;
-			if (filldir(dirent, ".", 1, i, ino, DT_DIR) < 0)
-				break;
-			filp->f_pos++;
-			i++;
-			/* fallthrough */
-		case 1:
-			ino = parent_ino(dentry);
-			if (filldir(dirent, "..", 2, i, ino, DT_DIR) < 0)
-				break;
-			filp->f_pos++;
-			i++;
-			/* fallthrough */
-		default:
-			spin_lock(&dcache_lock);
-			if (filp->f_pos == 2) {
-				list_del(q);
-				list_add(q, &dentry->d_subdirs);
-			}
-			for (p=q->next; p != &dentry->d_subdirs; p=p->next) {
-				struct dentry *next;
-				next = list_entry(p, struct dentry, d_child);
-				if (d_unhashed(next) || !next->d_inode)
-					continue;
-
-				spin_unlock(&dcache_lock);
-				if (filldir(dirent, next->d_name.name, next->d_name.len, filp->f_pos, next->d_inode->i_ino, dt_type(next->d_inode)) < 0)
-					return 0;
-				spin_lock(&dcache_lock);
-				/* next is still alive */
-				list_del(q);
-				list_add(q, p);
-				p = q;
-				filp->f_pos++;
-			}
-			spin_unlock(&dcache_lock);
+//	if (IS_ERR(cursor->d_fsdata))
+//		return PTR_ERR(cursor->d_fsdata);
+
+	if (filp->f_pos == 0) {
+		ino = dentry->d_inode->i_ino;
+		if (filldir(dirent, ".", 1, filp->f_pos, ino, DT_DIR) < 0)
+			goto out;
+		filp->f_pos = full_name_hash(".", 1);
 	}
+	if (filp->f_pos == full_name_hash(".", 1)) {
+		ino = parent_ino(dentry);
+		if (filldir(dirent, "..", 2, filp->f_pos, ino, DT_DIR) < 0)
+			goto out;
+		filp->f_pos = full_name_hash("..", 2);
+	}
+	spin_lock(&dcache_lock);
+	for (p=q->next; p != &dentry->d_subdirs; p=p->next) {
+		struct dentry *next;
+		next = list_entry(p, struct dentry, d_child);
+		if (d_unhashed(next) || !next->d_inode)
+			continue;
+		spin_unlock(&dcache_lock);
+		if (filldir(dirent, next->d_name.name,
+			    next->d_name.len, filp->f_pos,
+			    next->d_inode->i_ino,
+			    dt_type(next->d_inode)) < 0)
+			goto out;
+		spin_lock(&dcache_lock);
+		/* next is still alive */
+		list_del(q);
+		list_add(q, p);
+		p = q;
+		filp->f_pos = full_name_hash(next->d_name.name,
+					     next->d_name.len);
+	}
+	spin_unlock(&dcache_lock);
+out:
 	return 0;
 }
 
#1 lore raw
Quoted 8 lines · On Fri, Nov 04, 2005 at 12:38:51PM +0100, [email protected] wrote:

This patch effects all users of libfs' dcache directory implementation.

Old glibc implementations (e.g. glibc-2.2.5) are lseeking after every call to getdents(), subsequent calls to getdents() are starting to read from a wrong f_pos, when the directory is modified in between. Therefore not all directory entries are returned. IMHO this is a bug and it breaks applications, e.g. "rm -fr" on tmpfs.

Quoted 4 lines

SuSV3 only says:
"If a file is removed from or added to the directory after the most recent call to opendir() or rewinddir(), whether a subsequent call to readdir_r() returns an entry for that file is unspecified."

IOW, the applications in question are broken since they rely on unspecified behaviour, not provided by old libc versions.

#2 lore raw
Quoted 8 lines · Al Viro
Quoted 5 lines

SuSV3 only says:
"If a file is removed from or added to the directory after the most recent call to opendir() or rewinddir(), whether a subsequent call to readdir_r() returns an entry for that file is unspecified."

IOW, the applications in question are broken since they rely on unspecified behaviour, not provided by old libc versions.

No. SuSV3 only says that the behavior of readdir() is unspecified w.r.t. an entry for the removed/added file. I think readdir() should still return the entries which are not removed/added. What do you think?

Regards,
Jan Blunck

Signature · 4 lines
Jan Blunck                                               [email protected]
SuSE LINUX AG - A Novell company
Maxfeldstr. 5                                          +49-911-74053-608
D-90409 Nürnberg                                      http://www.suse.de
#3 lore raw
Quoted 6 lines
Quoted 6 lines
Quoted 5 lines

SuSV3 only says: "If a file is removed from or added to the directory after the most recent call to opendir() or rewinddir(), whether a subsequent call to readdir_r() returns an entry for that file is unspecified."

Quoted 7 lines
Quoted 2 lines

IOW, the applications in question are broken since they rely on unspecified behaviour, not provided by old libc versions.

No. SuSV3 only says that the behavior of readdir() is unspecified w.r.t. an entry for the removed/added file. I think readdir() should still return the entries which are not removed/added. What do you think?

What 'rm' is this? Mine (coreutils 5.2.1) doesn't do any seeking and I don't think that glibc does either.

Miklos

#4 lore raw
Quoted 9 lines · Miklos Szeredi
Quoted 9 lines
Quoted 8 lines
Quoted 5 lines

SuSV3 only says: "If a file is removed from or added to the directory after the most recent call to opendir() or rewinddir(), whether a subsequent call to readdir_r() returns an entry for that file is unspecified."

IOW, the applications in question are broken since they rely on unspecified behaviour, not provided by old libc versions.

Quoted 8 lines
Quoted 4 lines

No. SuSV3 only says that the behavior of readdir() is unspecified w.r.t. an entry for the removed/added file. I think readdir() should still return the entries which are not removed/added. What do you think?

What 'rm' is this? Mine (coreutils 5.2.1) doesn't do any seeking and I don't think that glibc does either.

As I said:
"Old glibc implementations (e.g. glibc-2.2.5) are lseeking after every call to getdents() ..."

Precisely this is a SLES8 on s390-64bit.

s390vm02:/# rpm -qf /bin/rm
fileutils-4.1.11-144
s390vm02:/# rpm -q glibc
glibc-2.2.5-234

But you can also try my testcase.

Regards,
Jan Blunck

Signature · 4 lines
Jan Blunck                                               [email protected]
SuSE LINUX AG - A Novell company
Maxfeldstr. 5                                          +49-911-74053-608
D-90409 Nürnberg                                      http://www.suse.de
#5 lore raw
Quoted 2 lines

As I said: "Old glibc implementations (e.g. glibc-2.2.5) are lseeking after every call to getdents() ..."

Hmm, why would it do that? This seems like it's glibc being stupid.

That said, you are right that the libfs readdir implementation is not strictly standards conforming. But neither is your patch: this algorithm will break if the entry at the current position is removed and then a new entry with the same name is created.

Quoted 7 lines

Precisely this is a SLES8 on s390-64bit.

s390vm02:/# rpm -qf /bin/rm
fileutils-4.1.11-144
s390vm02:/# rpm -q glibc
glibc-2.2.5-234

But you can also try my testcase.

Unfortunately I can't since I don't have such old glibc.

Miklos

#6 lore raw
Quoted 5 lines · Miklos Szeredi
Quoted 2 lines

As I said: "Old glibc implementations (e.g. glibc-2.2.5) are lseeking after every call to getdents() ..."

Hmm, why would it do that? This seems like it's glibc being stupid.

Well, glibc is that stupid and triggers the bug.

Quoted 4 lines

That said, you are right that the libfs readdir implementation is not strictly standards conforming. But neither is your patch: this algorithm will break if the entry at the current position is removed and then a new entry with the same name is created.

True. Seeking to that offset should at least fail and shouldn't stop at the new entry. But SuSV3 says that the offset given by telldir() is valid until the next rewinddir(). This is no problem for directories that can only grow. I tried to implement some kind of deferred dput'ing of the d_child's but that was too hackish and was wasting memory. So the best thing I can do now is fail if someone wants to seek to an offset of an already unlinked file.

So I can include the inode number in the hashing process somehow. Any ideas on that one?

Quoted 1 line

Unfortunately I can't since I don't have such old glibc.

The testcase is similar to what "rm *" with the old glibc would do. It just a testcase to show where the problem is.

#7 lore raw
Quoted 7 lines · On Fri, 4 November 2005 16:11:04 +0100, [email protected] wrote:

True. Seeking to that offset should at least fail and shouldn't stop at the new entry. But SuSV3 says that the offset given by telldir() is valid until the next rewinddir(). This is no problem for directories that can only grow. I tried to implement some kind of deferred dput'ing of the d_child's but that was too hackish and was wasting memory. So the best thing I can do now is fail if someone wants to seek to an offset of an already unlinked file.

Does that mean that, to satisfy the standard, you'd have to allow the seek, but return 0 bytes on further reads, as you're already at (or beyond, whatever) EOF?

Sounds quite ugly and it would be nice if this wasn't necessary.

Jörn

Signature · 3 lines
When people work hard for you for a pat on the back, you've got
to give them that pat.
-- Robert Heinlein
#8 lore raw
Quoted 9 lines · Jörn Engel
Quoted 7 lines · On Fri, 4 November 2005 16:11:04 +0100, [email protected] wrote:

True. Seeking to that offset should at least fail and shouldn't stop at the new entry. But SuSV3 says that the offset given by telldir() is valid until the next rewinddir(). This is no problem for directories that can only grow. I tried to implement some kind of deferred dput'ing of the d_child's but that was too hackish and was wasting memory. So the best thing I can do now is fail if someone wants to seek to an offset of an already unlinked file.

Quoted 3 lines

Does that mean that, to satisfy the standard, you'd have to allow the seek, but return 0 bytes on further reads, as you're already at (or beyond, whatever) EOF?

No. To satisfy the standard, it would be necessary to let the seek succeed and to return the already unlinked dentry or the next dentry (this is unspecified). I think we should return the next dentry, therefore I let the seek fail (seekdir() doesn't even have a return value) and the cursor/f_pos is still at the old offset.

The real problem is this IMHO:
...
telldir() = a
...
telldir() = b
readdir() = foo.txt
unlink(foo.txt)
seekdir(a)
seekdir(b)
readdir() = ???

With my patch the seekdir(b) doesn't find the offset and is placing the cursor at the end of the directory. In my understanding of the SuSV3 this should be possible and should return either "foo.txt" or the next entry after "foo.txt". I don't see any chance how I can implement that.

Regards,
Jan Blunck

Signature · 4 lines
Jan Blunck                                               [email protected]
SuSE LINUX AG - A Novell company
Maxfeldstr. 5                                          +49-911-74053-608
D-90409 Nürnberg                                      http://www.suse.de
#9 lore raw
Quoted 10 lines · On Fri, 4 November 2005 16:34:20 +0100, [email protected] wrote:
Quoted 9 lines · Jörn Engel
Quoted 7 lines · On Fri, 4 November 2005 16:11:04 +0100, [email protected] wrote:

True. Seeking to that offset should at least fail and shouldn't stop at the new entry. But SuSV3 says that the offset given by telldir() is valid until the next rewinddir(). This is no problem for directories that can only grow. I tried to implement some kind of deferred dput'ing of the d_child's but that was too hackish and was wasting memory. So the best thing I can do now is fail if someone wants to seek to an offset of an already unlinked file.

Quoted 7 lines
Quoted 3 lines

Does that mean that, to satisfy the standard, you'd have to allow the seek, but return 0 bytes on further reads, as you're already at (or beyond, whatever) EOF?

No. To satisfy the standard, it would be necessary to let the seek succeed and to return the already unlinked dentry or the next dentry (this is unspecified).

Do you have a link to the standard? Iirc, it is explicitly unspecified whether files created/deleted after opendir/rewinddir are returned. Why do you want to return one such unspecified file now? Especially when the implementation is ugly and the whole concept appears to be plain stupid.

Quoted 15 lines

I think we should return the next dentry, therefore I let the seek fail (seekdir() doesn't even have a return value) and the cursor/f_pos is still at the old offset.

The real problem is this IMHO:
...
telldir() = a
...
telldir() = b
readdir() = foo.txt
unlink(foo.txt)
seekdir(a)
seekdir(b)
readdir() = ???

Quoted 4 lines

With my patch the seekdir(b) doesn't find the offset and is placing the cursor at the end of the directory. In my understanding of the SuSV3 this should be possible and should return either "foo.txt" or the next entry after "foo.txt". I don't see any chance how I can implement that.

Does the above really happen, or is this just a theoretical case? At least it looks as if ext3 with dir_index will simply barf on it:

	/* Some one has messed with f_pos; reset the world */
	if (info->last_pos != filp->f_pos) {
		...

And imo, that is the correct behaviour. Anything else would leave the door wide open for trivial DOS attacks on kernel memory.

Jörn

Signature · 2 lines
The strong give up and move away, while the weak give up and stay.
-- unknown
#10 lore raw
In reply to Jörn Engel
Quoted 9 lines
Quoted 8 lines

True. Seeking to that offset should at least fail and shouldn't stop at the new entry. But SuSV3 says that the offset given by telldir() is valid until the next rewinddir(). This is no problem for directories that can only grow. I tried to implement some kind of deferred dput'ing of the d_child's but that was too hackish and was wasting memory. So the best thing I can do now is fail if someone wants to seek to an offset of an already unlinked file.

Quoted 5 lines

Does that mean that, to satisfy the standard, you'd have to allow the seek, but return 0 bytes on further reads, as you're already at (or beyond, whatever) EOF?

Sounds quite ugly and it would be nice if this wasn't necessary.

The directory position not _on_ an entry, but _between_ two entries. So it doesn't matter if the next entry is removed, the offset stays perfectly valid.

If there doesn't remain any entries after the position, readdir should return EOF. Otherwise it should continue reading normally.

What SUS doesn't specify is what happens entries added between opendir() and before rewinddir().

Miklos

#11 lore raw
In reply to [email protected]
Quoted 7 lines
Quoted 5 lines
Quoted 2 lines

As I said: "Old glibc implementations (e.g. glibc-2.2.5) are lseeking after every call to getdents() ..."

Hmm, why would it do that? This seems like it's glibc being stupid.

Well, glibc is that stupid and triggers the bug.

Seems to me, the simple solution is to upgrade your glibc.

Quoted 7 lines
Quoted 4 lines

That said, you are right that the libfs readdir implementation is not strictly standards conforming. But neither is your patch: this algorithm will break if the entry at the current position is removed and then a new entry with the same name is created.

True. Seeking to that offset should at least fail and shouldn't stop at the new entry.

No it should _not_ fail, it should continue from the next _existing_ entry.

Quoted 9 lines

But SuSV3 says that the offset given by telldir() is valid until the next rewinddir(). This is no problem for directories that can only grow. I tried to implement some kind of deferred dput'ing of the d_child's but that was too hackish and was wasting memory. So the best thing I can do now is fail if someone wants to seek to an offset of an already unlinked file.

So I can include the inode number in the hashing process somehow. Any ideas on that one?

No good. Same problem if you move out then move back the entry.

Quoted 5 lines
Quoted 2 lines

Unfortunately I can't since I don't have such old glibc.

The testcase is similar to what "rm *" with the old glibc would do. It just a testcase to show where the problem is.

I understand, but I have glibc-2.3.5 which apparently doesn't seek after readdir().

Miklos

#12 lore raw
Quoted 5 lines · Miklos Szeredi
Quoted 2 lines

Well, glibc is that stupid and triggers the bug.

Seems to me, the simple solution is to upgrade your glibc.

This is SLES8. You don't want to update the glibc.

Quoted 7 lines
Quoted 3 lines

True. Seeking to that offset should at least fail and shouldn't stop at the new entry.

No it should _not_ fail, it should continue from the next _existing_ entry.

And how do we achieve this for all the libfs users like tmpfs etc.? At least with my patch you can seek to the offsets of existing entries. Even that isn't possible at the moment if you remove directory entries. But I don't know how to implement that for the offsets of unlinked entries.

Quoted 3 lines

No good. Same problem if you move out then move back the entry.

Yeah, I know.

Regards,
Jan Blunck

Signature · 4 lines
Jan Blunck                                               [email protected]
SuSE LINUX AG - A Novell company
Maxfeldstr. 5                                          +49-911-74053-608
D-90409 Nürnberg                                      http://www.suse.de
#13 lore raw
Quoted 7 lines
Quoted 5 lines
Quoted 2 lines

Well, glibc is that stupid and triggers the bug.

Seems to me, the simple solution is to upgrade your glibc.

This is SLES8. You don't want to update the glibc.

OK, but then it's basically a SLES8 kernel issue, not a mainline kernel issue.

Probably very few people are using brand new kernels with glibc from the last millennium ;)

Miklos

#14 lore raw
Quoted 6 lines · Miklos Szeredi
Quoted 6 lines
Quoted 5 lines
Quoted 2 lines

Well, glibc is that stupid and triggers the bug.

Seems to me, the simple solution is to upgrade your glibc.

Quoted 8 lines
Quoted 1 line

This is SLES8. You don't want to update the glibc.

OK, but then it's basically a SLES8 kernel issue, not a mainline kernel issue.

Probably very few people are using brand new kernels with glibc from the last millennium ;)

Hmm, so I should only send patches for bugs that are often triggered upstream? Just start using seekdir() and modify the directory contents on an upstream tmpfs. You'll see that this isn't working well.

This is a bug and it should get fixed. I hope that it doesn't depend on how many people using what library :)

Regards,
Jan Blunck

Signature · 4 lines
Jan Blunck                                               [email protected]
SuSE LINUX AG - A Novell company
Maxfeldstr. 5                                          +49-911-74053-608
D-90409 Nürnberg                                      http://www.suse.de
#15 lore raw
Quoted 15 lines
Quoted 6 lines
Quoted 6 lines
Quoted 5 lines
Quoted 2 lines

Well, glibc is that stupid and triggers the bug.

Seems to me, the simple solution is to upgrade your glibc.

Quoted 8 lines
Quoted 1 line

This is SLES8. You don't want to update the glibc.

OK, but then it's basically a SLES8 kernel issue, not a mainline kernel issue.

Probably very few people are using brand new kernels with glibc from the last millennium ;)

Quoted 6 lines

Hmm, so I should only send patches for bugs that are often triggered upstream? Just start using seekdir() and modify the directory contents on an upstream tmpfs. You'll see that this isn't working well.

This is a bug and it should get fixed. I hope that it doesn't depend on how many people using what library :)

Ah, but it's a bug in the _system_ and glibc is part of the system too, not just the kernel.

Upgrading glibc solves the problem, so why work around it in kernel too?

Your patch is not a solution, since readdir will remain nonconforming. It is basically a workaround for a bug in glibc. It makes readdir nonconforming in a different way, but the end result in not necessarily better.

If you manage to make dcache_readdir conform to SUS without overly bloating the implementation, that's fine.

Miklos

#16 lore raw
Quoted 3 lines · Miklos Szeredi

Your patch is not a solution, since readdir will remain nonconforming. It is basically a workaround for a bug in glibc.

Updating the glibc is no solution because this isn't possible here. And I still have the opinion that it is the filesystems job to remember the right offset. At least this is what SuSV3 is telling me.

Quoted 3 lines

It makes readdir
nonconforming in a different way, but the end result in not necessarily better.

Hmm, yes you are right.

Quoted 3 lines

If you manage to make dcache_readdir conform to SUS without overly bloating the implementation, that's fine.

Will look into that, if its possible.

Regards,
Jan Blunck

Signature · 4 lines
Jan Blunck                                               [email protected]
SuSE LINUX AG - A Novell company
Maxfeldstr. 5                                          +49-911-74053-608
D-90409 Nürnberg                                      http://www.suse.de
#18 lore raw
In reply to [email protected]
Quoted 9 lines · On Fri, 2005-11-04 at 16:11 +0100, [email protected] wrote:
Quoted 5 lines · Miklos Szeredi
Quoted 2 lines

As I said: "Old glibc implementations (e.g. glibc-2.2.5) are lseeking after every call to getdents() ..."

Hmm, why would it do that? This seems like it's glibc being stupid.

Well, glibc is that stupid and triggers the bug.

It is due to the kernel's 32-bit struct dirent being smaller than glibc's 32-bit struct dirent (glibc has the extra ->d_type field). Because the dirent record length depends on the filename length, the exact expansion factor for the results of a call to getdents() may not be precomputed.
glibc uses a heuristic in order to estimate the expansion size, and then uses that to allocate an intermediate buffer in which to store the results of the getdents syscall.
If the contents of said intermediate buffer still happen to overflow the user-allocated buffer, then glibc calls lseek() in order to rewind the file pointer to the next entry it wants to read (and screws any filesystem that doesn't support lseek on directories).

This code appears still to be part of glibc, however it is rarely triggered these days because glibc's implementation now defaults to using the getdents64 syscall (if it exists) instead of the 32-bit version. Since the kernel's struct dirent64 is the same size as the glibc struct dirent64 (and larger than the 32-bit struct dirent), there is never any chance of buffer overflow.

The new bug is rather that glibc will return EOVERFLOW, and try to rewind your file pointer if your filesystem happens to return 64-bit offsets to getdents64().

Quoted 4 lines
Quoted 1 line

Unfortunately I can't since I don't have such old glibc.

The testcase is similar to what "rm *" with the old glibc would do. It just a testcase to show where the problem is.

'rm -rf' on a large directory used to be a great way to trigger it.

Cheers,
Trond

#19 lore raw
Quoted 3 lines

The new bug is rather that glibc will return EOVERFLOW, and try to rewind your file pointer if your filesystem happens to return 64-bit offsets to getdents64().

Well, that's sort of understandable. It has to map unique 64-bit offsets to unique 32-bits offsets, which is rather hard.

Miklos

#20 lore raw
In reply to Al Viro
Quoted 9 lines · Al Viro
Quoted 8 lines · On Fri, Nov 04, 2005 at 12:38:51PM +0100, [email protected] wrote:

This patch effects all users of libfs' dcache directory implementation.

Old glibc implementations (e.g. glibc-2.2.5) are lseeking after every call to getdents(), subsequent calls to getdents() are starting to read from a wrong f_pos, when the directory is modified in between. Therefore not all directory entries are returned. IMHO this is a bug and it breaks applications, e.g. "rm -fr" on tmpfs.

Quoted 7 lines
Quoted 4 lines

SuSV3 only says:
"If a file is removed from or added to the directory after the most recent call to opendir() or rewinddir(), whether a subsequent call to readdir_r() returns an entry for that file is unspecified."

IOW, the applications in question are broken since they rely on unspecified behaviour, not provided by old libc versions.

Are you sure that's the problem?

Directory starts with 26 files named A-F.
Reading through directory starts at A, makes it to J (position 10). File B gets deleted.
directory reading continues at new position 11, which is now L.

So directory read returns A-J, L-Z, and never returns K even though K didn't change.

The "that file" mentioned by SuSv3 above would be _B_ here. Not K. K didn't change.

That said, I'm pretty sure it's the old libc behavior that's defective. If a new entry B' had been inserted instead, the directory traversal would have seen L twice. Iterating by position is just wrong...

Rob

#21 lore raw
Quoted 2 lines · Rob Landley

That said, I'm pretty sure it's the old libc behavior that's defective. If a

So, what should the libc do? Reread the whole directory if it is changed? It would be a nice DoS attack to just create and delete files and prevent others from reading that directory.

IMHO it is the job of the filesystem implementation to allow correct SuSV3 specified directory reading.

Regards,
Jan Blunck

Signature · 4 lines
Jan Blunck                                               [email protected]
SuSE LINUX AG - A Novell company
Maxfeldstr. 5                                          +49-911-74053-608
D-90409 Nürnberg                                      http://www.suse.de
#22 lore raw
In reply to [email protected]
Quoted 17 lines · On Fri, 4 November 2005 12:38:51 +0100, [email protected] wrote:

#include <stdio.h>
#include <unistd.h>
#include <dirent.h>

int main(int argc, char *argv[])

{
	DIR *dir;
	struct dirent *entry;
	unsigned int offset;

        if (argc < 2) {
                printf("USAGE:  %s <directory>\n", argv[0]);
                return 1;
        }

	dir = opendir(argv[1]);
	if (!dir)
Quoted 8 lines
		return 1;

	while((entry = readdir(dir)) != NULL) {
		seekdir(dir, entry->d_off);
		printf("name=%s\tino=%d off=%d\n", entry->d_name, entry->d_ino,
		       entry->d_off);
		if (*entry->d_name == '.')
			continue;

That catches "." and "..", but also ".foo". Doesn't matter for the test, just wanted to mention it.

Quoted 7 lines
		if(unlink(entry->d_name) != 0)
			break;
	}

	closedir(dir);
	return 0;
}

Jörn

Signature · 3 lines
With a PC, I always felt limited by the software available. On Unix, 
I am limited only by my knowledge.
-- Peter J. Schoenster