Skip to content

Commit bf25d34

Browse files
authored
[WasmFS] Eagerly read directory entries when opening directories (#19556)
This ensures that subsequent getdents calls see a stable set of entries, even as entries are removed or inserted after the open. Although on its face this sounds like it would be incorrect behavior, it is in fact allowed by the spec and solves a bug where deleting entries while iterating through them would cause some entries to be skipped. Fixes #19106.
1 parent 87cd6de commit bf25d34

File tree

9 files changed

+154
-27
lines changed

9 files changed

+154
-27
lines changed

.circleci/config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,7 @@ jobs:
515515
wasmfs.test_unistd_close
516516
wasmfs.test_unistd_truncate
517517
wasmfs.test_readdir
518+
wasmfs.test_readdir_unlink
518519
wasmfs.test_unistd_pipe
519520
wasmfs.test_fs_write
520521
wasmfs.test_webidl

system/lib/wasmfs/file_table.cpp

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,30 @@ FileTable::Handle::addEntry(std::shared_ptr<OpenFileState> openFileState) {
6060
return -EBADF;
6161
}
6262

63+
int OpenFileState::create(std::shared_ptr<File> file,
64+
oflags_t flags,
65+
std::shared_ptr<OpenFileState>& out) {
66+
assert(file);
67+
std::vector<Directory::Entry> dirents;
68+
if (auto f = file->dynCast<DataFile>()) {
69+
if (int err = f->locked().open(flags & O_ACCMODE)) {
70+
return err;
71+
}
72+
} else if (auto d = file->dynCast<Directory>()) {
73+
// We are opening a directory; cache its contents for subsequent reads.
74+
auto lockedDir = d->locked();
75+
dirents = {{".", File::DirectoryKind, d->getIno()},
76+
{"..", File::DirectoryKind, lockedDir.getParent()->getIno()}};
77+
auto entries = lockedDir.getEntries();
78+
if (int err = entries.getError()) {
79+
return err;
80+
}
81+
dirents.insert(dirents.end(), entries->begin(), entries->end());
82+
}
83+
84+
out = std::make_shared<OpenFileState>(
85+
private_key{0}, flags, file, std::move(dirents));
86+
return 0;
87+
}
88+
6389
} // namespace wasmfs

system/lib/wasmfs/file_table.h

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -58,21 +58,24 @@ class OpenFileState : public std::enable_shared_from_this<OpenFileState> {
5858
friend FileTable;
5959

6060
public:
61-
OpenFileState(private_key, oflags_t flags, std::shared_ptr<File> file)
62-
: file(file), flags(flags) {}
61+
// Cache directory entries at the moment the directory is opened so that
62+
// subsequent getdents calls have a stable view of the contents. Including
63+
// files removed after the open and excluding files added after the open is
64+
// allowed, and trying to recalculate the directory contents on each getdents
65+
// call could lead to missed directory entries if there are concurrent
66+
// deletions that effectively move entries back past the current read position
67+
// in the open directory.
68+
const std::vector<Directory::Entry> dirents;
69+
70+
OpenFileState(private_key,
71+
oflags_t flags,
72+
std::shared_ptr<File> file,
73+
std::vector<Directory::Entry>&& dirents)
74+
: file(file), flags(flags), dirents(std::move(dirents)) {}
6375

6476
[[nodiscard]] static int create(std::shared_ptr<File> file,
6577
oflags_t flags,
66-
std::shared_ptr<OpenFileState>& out) {
67-
assert(file);
68-
if (auto f = file->dynCast<DataFile>()) {
69-
if (int err = f->locked().open(flags & O_ACCMODE)) {
70-
return err;
71-
}
72-
}
73-
out = std::make_shared<OpenFileState>(private_key{0}, flags, file);
74-
return 0;
75-
}
78+
std::shared_ptr<OpenFileState>& out);
7679

7780
class Handle {
7881
std::shared_ptr<OpenFileState> openFileState;

system/lib/wasmfs/syscalls.cpp

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -888,19 +888,11 @@ int __syscall_getdents64(int fd, intptr_t dirp, size_t count) {
888888
return 0;
889889
}
890890

891-
std::vector<Directory::Entry> entries = {
892-
{".", File::DirectoryKind, dir->getIno()},
893-
{"..", File::DirectoryKind, parent->getIno()}};
894-
auto dirEntries = lockedDir.getEntries();
895-
if (int err = dirEntries.getError()) {
896-
return err;
897-
}
898-
entries.insert(entries.end(), dirEntries->begin(), dirEntries->end());
899-
900891
off_t bytesRead = 0;
901-
for (; index < entries.size() && bytesRead + sizeof(dirent) <= count;
892+
const auto& dirents = openFile->dirents;
893+
for (; index < dirents.size() && bytesRead + sizeof(dirent) <= count;
902894
index++) {
903-
auto& entry = entries[index];
895+
const auto& entry = dirents[index];
904896
result->d_ino = entry.ino;
905897
result->d_off = index + 1;
906898
result->d_reclen = sizeof(dirent);

test/dirent/test_readdir_unlink.c

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright 2023 The Emscripten Authors. All rights reserved.
3+
* Emscripten is available under two separate licenses, the MIT license and the
4+
* University of Illinois/NCSA Open Source License. Both these licenses can be
5+
* found in the LICENSE file.
6+
*/
7+
8+
#include <dirent.h>
9+
#include <errno.h>
10+
#include <fcntl.h>
11+
#include <stdio.h>
12+
#include <string.h>
13+
#include <sys/stat.h>
14+
#include <unistd.h>
15+
16+
int main() {
17+
if (mkdir("test", 0777) != 0) {
18+
printf("Unable to create dir 'test'\n");
19+
return 1;
20+
}
21+
22+
for (int i = 0; i < 10; i++) {
23+
char path[10];
24+
snprintf(path, 10, "test/%d", i);
25+
int fd = open(path, O_CREAT, 0777);
26+
if (fd < 0) {
27+
printf("Unable to create file '%s'\n", path);
28+
return 1;
29+
}
30+
close(fd);
31+
}
32+
33+
DIR* dir = opendir("test");
34+
if (!dir) {
35+
printf("Unable to open dir 'test'\n");
36+
return 1;
37+
}
38+
39+
struct dirent* dirent;
40+
41+
while ((dirent = readdir(dir)) != NULL) {
42+
if (strcmp(dirent->d_name, ".") == 0 || strcmp(dirent->d_name, "..") == 0) {
43+
continue;
44+
}
45+
char path[10];
46+
snprintf(path, 10, "test/%s", dirent->d_name);
47+
if (unlink(path)) {
48+
printf("Unable to unlink '%s'\n", path);
49+
return 1;
50+
}
51+
printf("Unlinked '%s'\n", path);
52+
53+
// Add a new file in the middle. This will not be visited (although it would
54+
// be valid to visit it).
55+
if (strcmp(dirent->d_name, "5") == 0) {
56+
int fd = open("test/X", O_CREAT, 0777);
57+
if (fd < 0) {
58+
printf("Unable to create file 'test/X'\n");
59+
return 1;
60+
}
61+
close(fd);
62+
}
63+
}
64+
65+
closedir(dir);
66+
67+
// The new file should still exist.
68+
if (access("test/X", F_OK) != 0) {
69+
printf("Expected file 'test/X' to exist\n");
70+
return 1;
71+
}
72+
if (unlink("test/X")) {
73+
printf("Unable to unlink 'test/X'\n");
74+
return 1;
75+
}
76+
77+
if (rmdir("test")) {
78+
printf("Unable to remove dir 'test'\n");
79+
return 1;
80+
}
81+
82+
printf("success\n");
83+
84+
return 0;
85+
}

test/dirent/test_readdir_unlink.out

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Unlinked 'test/0'
2+
Unlinked 'test/1'
3+
Unlinked 'test/2'
4+
Unlinked 'test/3'
5+
Unlinked 'test/4'
6+
Unlinked 'test/5'
7+
Unlinked 'test/6'
8+
Unlinked 'test/7'
9+
Unlinked 'test/8'
10+
Unlinked 'test/9'
11+
success

test/other/metadce/test_metadce_files_wasmfs.funcs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
$__cxa_pure_virtual
21
$__cxx_global_array_dtor
32
$__cxx_global_array_dtor.1
43
$__cxx_global_array_dtor.2
@@ -15,7 +14,6 @@ $__cxxabiv1::__class_type_info::process_found_base_class\28__cxxabiv1::__dynamic
1514
$__cxxabiv1::__class_type_info::process_static_type_above_dst\28__cxxabiv1::__dynamic_cast_info*\2c\20void\20const*\2c\20void\20const*\2c\20int\29\20const
1615
$__cxxabiv1::__class_type_info::search_above_dst\28__cxxabiv1::__dynamic_cast_info*\2c\20void\20const*\2c\20void\20const*\2c\20int\2c\20bool\29\20const
1716
$__cxxabiv1::__class_type_info::search_below_dst\28__cxxabiv1::__dynamic_cast_info*\2c\20void\20const*\2c\20int\2c\20bool\29\20const
18-
$__cxxabiv1::__class_type_info::~__class_type_info\28\29
1917
$__cxxabiv1::__si_class_type_info::has_unambiguous_public_base\28__cxxabiv1::__dynamic_cast_info*\2c\20void*\2c\20int\29\20const
2018
$__cxxabiv1::__si_class_type_info::search_above_dst\28__cxxabiv1::__dynamic_cast_info*\2c\20void\20const*\2c\20void\20const*\2c\20int\2c\20bool\29\20const
2119
$__cxxabiv1::__si_class_type_info::search_below_dst\28__cxxabiv1::__dynamic_cast_info*\2c\20void\20const*\2c\20int\2c\20bool\29\20const
@@ -34,6 +32,7 @@ $__unlockfile
3432
$__wasi_fd_close
3533
$__wasi_fd_write
3634
$__wasm_call_ctors
35+
$decltype\28auto\29\20std::__2::__variant_detail::__visitation::__base::__dispatcher<0ul>::__dispatch\5babi:v160004\5d<std::__2::__variant_detail::__dtor<std::__2::__variant_detail::__traits<std::__2::vector<wasmfs::Directory::Entry\2c\20std::__2::allocator<wasmfs::Directory::Entry>>\2c\20int>\2c\20\28std::__2::__variant_detail::_Trait\291>::__destroy\5babi:v160004\5d\28\29::'lambda'\28auto&\29&&\2c\20std::__2::__variant_detail::__base<\28std::__2::__variant_detail::_Trait\291\2c\20std::__2::vector<wasmfs::Directory::Entry\2c\20std::__2::allocator<wasmfs::Directory::Entry>>\2c\20int>&>\28auto\2c\20std::__2::__variant_detail::__base<\28std::__2::__variant_detail::_Trait\291\2c\20std::__2::vector<wasmfs::Directory::Entry\2c\20std::__2::allocator<wasmfs::Directory::Entry>>\2c\20int>&\29
3736
$decltype\28auto\29\20std::__2::__variant_detail::__visitation::__base::__dispatcher<1ul>::__dispatch\5babi:v160004\5d<std::__2::__variant_detail::__dtor<std::__2::__variant_detail::__traits<long\2c\20std::__2::shared_ptr<wasmfs::File>>\2c\20\28std::__2::__variant_detail::_Trait\291>::__destroy\5babi:v160004\5d\28\29::'lambda'\28auto&\29&&\2c\20std::__2::__variant_detail::__base<\28std::__2::__variant_detail::_Trait\291\2c\20long\2c\20std::__2::shared_ptr<wasmfs::File>>&>\28auto\2c\20std::__2::__variant_detail::__base<\28std::__2::__variant_detail::_Trait\291\2c\20long\2c\20std::__2::shared_ptr<wasmfs::File>>&\29
3837
$dlfree
3938
$dlmalloc
@@ -46,6 +45,7 @@ $memmove
4645
$operator\20new\28unsigned\20long\29
4746
$pthread_mutex_init
4847
$sbrk
48+
$std::__2::__allocation_result<std::__2::allocator_traits<std::__2::allocator<char>>::pointer>\20std::__2::__allocate_at_least\5babi:v160004\5d<std::__2::allocator<char>>\28std::__2::allocator<char>&\2c\20unsigned\20long\29
4949
$std::__2::__shared_ptr_emplace<wasmfs::MemoryDataFile\2c\20std::__2::allocator<wasmfs::MemoryDataFile>>::~__shared_ptr_emplace\28\29
5050
$std::__2::__shared_ptr_emplace<wasmfs::MemoryDataFile\2c\20std::__2::allocator<wasmfs::MemoryDataFile>>::~__shared_ptr_emplace\28\29.1
5151
$std::__2::__shared_ptr_emplace<wasmfs::MemoryDirectory\2c\20std::__2::allocator<wasmfs::MemoryDirectory>>::__on_zero_shared\28\29
@@ -71,12 +71,18 @@ $std::__2::__shared_ptr_emplace<wasmfs::SpecialFiles::\28anonymous\20namespace\2
7171
$std::__2::__shared_weak_count::__release_weak\28\29
7272
$std::__2::__shared_weak_count::lock\28\29
7373
$std::__2::__tree<std::__2::__value_type<std::__2::basic_string<char\2c\20std::__2::char_traits<char>\2c\20std::__2::allocator<char>>\2c\20wasmfs::Directory::DCacheEntry>\2c\20std::__2::__map_value_compare<std::__2::basic_string<char\2c\20std::__2::char_traits<char>\2c\20std::__2::allocator<char>>\2c\20std::__2::__value_type<std::__2::basic_string<char\2c\20std::__2::char_traits<char>\2c\20std::__2::allocator<char>>\2c\20wasmfs::Directory::DCacheEntry>\2c\20std::__2::less<std::__2::basic_string<char\2c\20std::__2::char_traits<char>\2c\20std::__2::allocator<char>>>\2c\20true>\2c\20std::__2::allocator<std::__2::__value_type<std::__2::basic_string<char\2c\20std::__2::char_traits<char>\2c\20std::__2::allocator<char>>\2c\20wasmfs::Directory::DCacheEntry>>>::destroy\28std::__2::__tree_node<std::__2::__value_type<std::__2::basic_string<char\2c\20std::__2::char_traits<char>\2c\20std::__2::allocator<char>>\2c\20wasmfs::Directory::DCacheEntry>\2c\20void*>*\29
74-
$std::__2::basic_string<char\2c\20std::__2::char_traits<char>\2c\20std::__2::allocator<char>>::__begin_lifetime\5babi:v160004\5d\28char*\2c\20unsigned\20long\29
74+
$std::__2::basic_string<char\2c\20std::__2::char_traits<char>\2c\20std::__2::allocator<char>>&\20std::__2::basic_string<char\2c\20std::__2::char_traits<char>\2c\20std::__2::allocator<char>>::__assign_no_alias<false>\28char\20const*\2c\20unsigned\20long\29
75+
$std::__2::basic_string<char\2c\20std::__2::char_traits<char>\2c\20std::__2::allocator<char>>&\20std::__2::basic_string<char\2c\20std::__2::char_traits<char>\2c\20std::__2::allocator<char>>::__assign_no_alias<true>\28char\20const*\2c\20unsigned\20long\29
76+
$std::__2::basic_string<char\2c\20std::__2::char_traits<char>\2c\20std::__2::allocator<char>>::__grow_by_and_replace\28unsigned\20long\2c\20unsigned\20long\2c\20unsigned\20long\2c\20unsigned\20long\2c\20unsigned\20long\2c\20unsigned\20long\2c\20char\20const*\29
7577
$std::__2::basic_string<char\2c\20std::__2::char_traits<char>\2c\20std::__2::allocator<char>>::__init_copy_ctor_external\28char\20const*\2c\20unsigned\20long\29
78+
$std::__2::basic_string<char\2c\20std::__2::char_traits<char>\2c\20std::__2::allocator<char>>::__invalidate_iterators_past\5babi:v160004\5d\28unsigned\20long\29
79+
$std::__2::basic_string<char\2c\20std::__2::char_traits<char>\2c\20std::__2::allocator<char>>::__throw_length_error\5babi:v160004\5d\28\29\20const
80+
$std::__2::char_traits<char>::copy\28char*\2c\20char\20const*\2c\20unsigned\20long\29
7681
$std::__2::shared_ptr<wasmfs::SpecialFiles::\28anonymous\20namespace\29::RandomFile>\20std::__2::make_shared\5babi:v160004\5d<wasmfs::SpecialFiles::\28anonymous\20namespace\29::RandomFile\2c\20void>\28\29
7782
$std::__2::vector<unsigned\20char\2c\20std::__2::allocator<unsigned\20char>>::__append\28unsigned\20long\29
7883
$std::__2::vector<wasmfs::MemoryDirectory::ChildEntry\2c\20std::__2::allocator<wasmfs::MemoryDirectory::ChildEntry>>::erase\5babi:v160004\5d\28std::__2::__wrap_iter<wasmfs::MemoryDirectory::ChildEntry\20const*>\29
7984
$strlen
85+
$void\20std::__2::__libcpp_operator_delete\5babi:v160004\5d<void*>\28void*\29
8086
$void\20std::__2::vector<std::__2::shared_ptr<wasmfs::OpenFileState>\2c\20std::__2::allocator<std::__2::shared_ptr<wasmfs::OpenFileState>>>::__emplace_back_slow_path<>\28\29
8187
$wasmfs::DataFile::Handle::flush\28\29
8288
$wasmfs::DataFile::Handle::write\28unsigned\20char\20const*\2c\20unsigned\20long\2c\20long\20long\29
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
47462
1+
51960

test/test_core.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5814,6 +5814,9 @@ def test_readdir(self):
58145814
def test_readdir_empty(self):
58155815
self.do_run_in_out_file_test('dirent/test_readdir_empty.c')
58165816

5817+
def test_readdir_unlink(self):
5818+
self.do_run_in_out_file_test('dirent/test_readdir_unlink.c')
5819+
58175820
def test_stat(self):
58185821
self.set_setting("FORCE_FILESYSTEM")
58195822
self.do_runf(test_file('stat/test_stat.c'), 'success')

0 commit comments

Comments
 (0)