Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
emscripten-core
GitHub Repository: emscripten-core/emscripten
Path: blob/main/system/lib/wasmfs/syscalls.cpp
6174 views
1
// Copyright 2021 The Emscripten Authors. All rights reserved.
2
// Emscripten is available under two separate licenses, the MIT license and the
3
// University of Illinois/NCSA Open Source License. Both these licenses can be
4
// found in the LICENSE file.
5
6
// Syscall implementations.
7
8
#define _LARGEFILE64_SOURCE // For F_GETLK64 etc
9
10
#include <dirent.h>
11
#include <emscripten/emscripten.h>
12
#include <emscripten/heap.h>
13
#include <emscripten/html5.h>
14
#include <errno.h>
15
#include <mutex>
16
#include <poll.h>
17
#include <stdarg.h>
18
#include <stdlib.h>
19
#include <sys/ioctl.h>
20
#include <sys/mman.h>
21
#include <sys/stat.h>
22
#include <sys/statfs.h>
23
#include <syscall_arch.h>
24
#include <unistd.h>
25
#include <utility>
26
#include <vector>
27
#include <wasi/api.h>
28
29
#include "backend.h"
30
#include "file.h"
31
#include "file_table.h"
32
#include "paths.h"
33
#include "pipe_backend.h"
34
#include "special_files.h"
35
#include "wasmfs.h"
36
37
// File permission macros for wasmfs.
38
// Used to improve readability compared to those in stat.h
39
#define WASMFS_PERM_READ 0444
40
41
#define WASMFS_PERM_WRITE 0222
42
43
#define WASMFS_PERM_EXECUTE 0111
44
45
// In Linux, the maximum length for a filename is 255 bytes.
46
#define WASMFS_NAME_MAX 255
47
48
extern "C" {
49
50
using namespace wasmfs;
51
52
int __syscall_dup3(int oldfd, int newfd, int flags) {
53
if (flags & !O_CLOEXEC) {
54
// TODO: Test this case.
55
return -EINVAL;
56
}
57
58
auto fileTable = wasmFS.getFileTable().locked();
59
auto oldOpenFile = fileTable.getEntry(oldfd);
60
if (!oldOpenFile) {
61
return -EBADF;
62
}
63
if (newfd < 0 || newfd >= WASMFS_FD_MAX) {
64
return -EBADF;
65
}
66
if (oldfd == newfd) {
67
return -EINVAL;
68
}
69
70
// If the file descriptor newfd was previously open, it will just be
71
// overwritten silently.
72
(void)fileTable.setEntry(newfd, oldOpenFile);
73
return newfd;
74
}
75
76
int __syscall_dup(int fd) {
77
auto fileTable = wasmFS.getFileTable().locked();
78
79
// Check that an open file exists corresponding to the given fd.
80
auto openFile = fileTable.getEntry(fd);
81
if (!openFile) {
82
return -EBADF;
83
}
84
return fileTable.addEntry(openFile);
85
}
86
87
// This enum specifies whether file offset will be provided by the open file
88
// state or provided by argument in the case of pread or pwrite.
89
enum class OffsetHandling { OpenFileState, Argument };
90
91
// Internal write function called by __wasi_fd_write and __wasi_fd_pwrite
92
// Receives an open file state offset.
93
// Optionally sets open file state offset.
94
static __wasi_errno_t writeAtOffset(OffsetHandling setOffset,
95
__wasi_fd_t fd,
96
const __wasi_ciovec_t* iovs,
97
size_t iovs_len,
98
__wasi_size_t* nwritten,
99
__wasi_filesize_t offset = 0) {
100
auto openFile = wasmFS.getFileTable().locked().getEntry(fd);
101
if (!openFile) {
102
return __WASI_ERRNO_BADF;
103
}
104
105
if (iovs_len < 0 || offset < 0) {
106
return __WASI_ERRNO_INVAL;
107
}
108
109
auto lockedOpenFile = openFile->locked();
110
auto file = lockedOpenFile.getFile()->dynCast<DataFile>();
111
if (!file) {
112
return __WASI_ERRNO_ISDIR;
113
}
114
115
auto lockedFile = file->locked();
116
117
if (setOffset == OffsetHandling::OpenFileState) {
118
if (lockedOpenFile.getFlags() & O_APPEND) {
119
off_t size = lockedFile.getSize();
120
if (size < 0) {
121
// Translate to WASI standard of positive return codes.
122
return -size;
123
}
124
offset = size;
125
lockedOpenFile.setPosition(offset);
126
} else {
127
offset = lockedOpenFile.getPosition();
128
}
129
}
130
131
// TODO: Check open file access mode for write permissions.
132
133
size_t bytesWritten = 0;
134
for (size_t i = 0; i < iovs_len; i++) {
135
const uint8_t* buf = iovs[i].buf;
136
off_t len = iovs[i].buf_len;
137
138
// Check if buf_len specifies a positive length buffer but buf is a
139
// null pointer
140
if (!buf && len > 0) {
141
return __WASI_ERRNO_INVAL;
142
}
143
144
// Check if the sum of the buf_len values overflows an off_t (63 bits).
145
if (addWillOverFlow(offset, (__wasi_filesize_t)bytesWritten)) {
146
return __WASI_ERRNO_FBIG;
147
}
148
149
auto result = lockedFile.write(buf, len, offset + bytesWritten);
150
if (result < 0) {
151
// This individual write failed. Report the error unless we've already
152
// written some bytes, in which case report a successful short write.
153
if (bytesWritten > 0) {
154
break;
155
}
156
return -result;
157
}
158
// The write was successful.
159
bytesWritten += result;
160
if (result < len) {
161
// The write was short, so stop here.
162
break;
163
}
164
}
165
*nwritten = bytesWritten;
166
if (setOffset == OffsetHandling::OpenFileState &&
167
lockedOpenFile.getFile()->isSeekable()) {
168
lockedOpenFile.setPosition(offset + bytesWritten);
169
}
170
if (bytesWritten) {
171
lockedFile.updateMTime();
172
}
173
return __WASI_ERRNO_SUCCESS;
174
}
175
176
// Internal read function called by __wasi_fd_read and __wasi_fd_pread
177
// Receives an open file state offset.
178
// Optionally sets open file state offset.
179
// TODO: combine this with writeAtOffset because the code is nearly identical.
180
static __wasi_errno_t readAtOffset(OffsetHandling setOffset,
181
__wasi_fd_t fd,
182
const __wasi_iovec_t* iovs,
183
size_t iovs_len,
184
__wasi_size_t* nread,
185
__wasi_filesize_t offset = 0) {
186
auto openFile = wasmFS.getFileTable().locked().getEntry(fd);
187
if (!openFile) {
188
return __WASI_ERRNO_BADF;
189
}
190
191
auto lockedOpenFile = openFile->locked();
192
193
if (setOffset == OffsetHandling::OpenFileState) {
194
offset = lockedOpenFile.getPosition();
195
}
196
197
if (iovs_len < 0 || offset < 0) {
198
return __WASI_ERRNO_INVAL;
199
}
200
201
// TODO: Check open file access mode for read permissions.
202
203
auto file = lockedOpenFile.getFile()->dynCast<DataFile>();
204
205
// If file is nullptr, then the file was not a DataFile.
206
if (!file) {
207
return __WASI_ERRNO_ISDIR;
208
}
209
210
auto lockedFile = file->locked();
211
212
size_t bytesRead = 0;
213
for (size_t i = 0; i < iovs_len; i++) {
214
uint8_t* buf = iovs[i].buf;
215
size_t len = iovs[i].buf_len;
216
217
if (!buf && len > 0) {
218
return __WASI_ERRNO_INVAL;
219
}
220
221
// TODO: Check for overflow when adding offset + bytesRead.
222
auto result = lockedFile.read(buf, len, offset + bytesRead);
223
if (result < 0) {
224
// This individual read failed. Report the error unless we've already read
225
// some bytes, in which case report a successful short read.
226
if (bytesRead > 0) {
227
break;
228
}
229
return -result;
230
}
231
232
// The read was successful.
233
234
// Backends must only return len or less.
235
assert(result <= len);
236
237
bytesRead += result;
238
if (result < len) {
239
// The read was short, so stop here.
240
break;
241
}
242
}
243
*nread = bytesRead;
244
if (setOffset == OffsetHandling::OpenFileState &&
245
lockedOpenFile.getFile()->isSeekable()) {
246
lockedOpenFile.setPosition(offset + bytesRead);
247
}
248
return __WASI_ERRNO_SUCCESS;
249
}
250
251
__wasi_errno_t __wasi_fd_write(__wasi_fd_t fd,
252
const __wasi_ciovec_t* iovs,
253
size_t iovs_len,
254
__wasi_size_t* nwritten) {
255
return writeAtOffset(
256
OffsetHandling::OpenFileState, fd, iovs, iovs_len, nwritten);
257
}
258
259
__wasi_errno_t __wasi_fd_read(__wasi_fd_t fd,
260
const __wasi_iovec_t* iovs,
261
size_t iovs_len,
262
__wasi_size_t* nread) {
263
return readAtOffset(OffsetHandling::OpenFileState, fd, iovs, iovs_len, nread);
264
}
265
266
__wasi_errno_t __wasi_fd_pwrite(__wasi_fd_t fd,
267
const __wasi_ciovec_t* iovs,
268
size_t iovs_len,
269
__wasi_filesize_t offset,
270
__wasi_size_t* nwritten) {
271
return writeAtOffset(
272
OffsetHandling::Argument, fd, iovs, iovs_len, nwritten, offset);
273
}
274
275
__wasi_errno_t __wasi_fd_pread(__wasi_fd_t fd,
276
const __wasi_iovec_t* iovs,
277
size_t iovs_len,
278
__wasi_filesize_t offset,
279
__wasi_size_t* nread) {
280
return readAtOffset(
281
OffsetHandling::Argument, fd, iovs, iovs_len, nread, offset);
282
}
283
284
__wasi_errno_t __wasi_fd_close(__wasi_fd_t fd) {
285
std::shared_ptr<DataFile> closee;
286
{
287
// Do not hold the file table lock while performing the close.
288
auto fileTable = wasmFS.getFileTable().locked();
289
auto entry = fileTable.getEntry(fd);
290
if (!entry) {
291
return __WASI_ERRNO_BADF;
292
}
293
closee = fileTable.setEntry(fd, nullptr);
294
}
295
if (closee) {
296
// Translate to WASI standard of positive return codes.
297
int ret = -closee->locked().close();
298
assert(ret >= 0);
299
return ret;
300
}
301
return __WASI_ERRNO_SUCCESS;
302
}
303
304
__wasi_errno_t __wasi_fd_sync(__wasi_fd_t fd) {
305
auto openFile = wasmFS.getFileTable().locked().getEntry(fd);
306
if (!openFile) {
307
return __WASI_ERRNO_BADF;
308
}
309
310
// Nothing to flush for anything but a data file, but also not an error either
311
// way. TODO: in the future we may want syncing of directories.
312
auto dataFile = openFile->locked().getFile()->dynCast<DataFile>();
313
if (dataFile) {
314
auto ret = dataFile->locked().flush();
315
assert(ret <= 0);
316
// Translate to WASI standard of positive return codes.
317
return -ret;
318
}
319
320
return __WASI_ERRNO_SUCCESS;
321
}
322
323
int __syscall_fdatasync(int fd) {
324
// TODO: Optimize this to avoid unnecessarily flushing unnecessary metadata.
325
return __wasi_fd_sync(fd);
326
}
327
328
backend_t wasmfs_get_backend_by_fd(int fd) {
329
auto openFile = wasmFS.getFileTable().locked().getEntry(fd);
330
if (!openFile) {
331
return NullBackend;
332
}
333
return openFile->locked().getFile()->getBackend();
334
}
335
336
// This function is exposed to users to allow them to obtain a backend_t for a
337
// specified path.
338
backend_t wasmfs_get_backend_by_path(const char* path) {
339
auto parsed = path::parseFile(path);
340
if (parsed.getError()) {
341
// Could not find the file.
342
return NullBackend;
343
}
344
return parsed.getFile()->getBackend();
345
}
346
347
static timespec ms_to_timespec(double ms) {
348
long long seconds = ms / 1000;
349
timespec ts;
350
ts.tv_sec = seconds; // seconds
351
ts.tv_nsec = (ms - (seconds * 1000)) * 1000 * 1000; // nanoseconds
352
return ts;
353
}
354
355
int __syscall_newfstatat(int dirfd, intptr_t path, intptr_t buf, int flags) {
356
// Only accept valid flags.
357
if (flags & ~(AT_EMPTY_PATH | AT_NO_AUTOMOUNT | AT_SYMLINK_NOFOLLOW)) {
358
// TODO: Test this case.
359
return -EINVAL;
360
}
361
auto parsed = path::getFileAt(dirfd, (char*)path, flags);
362
if (auto err = parsed.getError()) {
363
return err;
364
}
365
auto file = parsed.getFile();
366
367
// Extract the information from the file.
368
auto lockedFile = file->locked();
369
auto buffer = (struct stat*)buf;
370
371
off_t size = lockedFile.getSize();
372
if (size < 0) {
373
return size;
374
}
375
buffer->st_size = size;
376
377
// ATTN: hard-coded constant values are copied from the existing JS file
378
// system. Specific values were chosen to match existing library_fs.js
379
// values.
380
// ID of device containing file: Hardcode 1 for now, no meaning at the
381
// moment for Emscripten.
382
buffer->st_dev = 1;
383
buffer->st_mode = lockedFile.getMode();
384
buffer->st_ino = file->getIno();
385
// The number of hard links is 1 since they are unsupported.
386
buffer->st_nlink = 1;
387
buffer->st_uid = 0;
388
buffer->st_gid = 0;
389
// Device ID (if special file) No meaning right now for Emscripten.
390
buffer->st_rdev = 0;
391
// The syscall docs state this is hardcoded to # of 512 byte blocks.
392
buffer->st_blocks = (buffer->st_size + 511) / 512;
393
// Specifies the preferred blocksize for efficient disk I/O.
394
buffer->st_blksize = 4096;
395
buffer->st_atim = ms_to_timespec(lockedFile.getATime());
396
buffer->st_mtim = ms_to_timespec(lockedFile.getMTime());
397
buffer->st_ctim = ms_to_timespec(lockedFile.getCTime());
398
return __WASI_ERRNO_SUCCESS;
399
}
400
401
int __syscall_stat64(intptr_t path, intptr_t buf) {
402
return __syscall_newfstatat(AT_FDCWD, path, buf, 0);
403
}
404
405
int __syscall_lstat64(intptr_t path, intptr_t buf) {
406
return __syscall_newfstatat(AT_FDCWD, path, buf, AT_SYMLINK_NOFOLLOW);
407
}
408
409
int __syscall_fstat64(int fd, intptr_t buf) {
410
return __syscall_newfstatat(fd, (intptr_t) "", buf, AT_EMPTY_PATH);
411
}
412
413
// When calling doOpen(), we may request an FD be returned, or we may not need
414
// that return value (in which case no FD need be allocated, and we return 0 on
415
// success).
416
enum class OpenReturnMode { FD, Nothing };
417
418
static __wasi_fd_t doOpen(path::ParsedParent parsed,
419
int flags,
420
mode_t mode,
421
backend_t backend = NullBackend,
422
OpenReturnMode returnMode = OpenReturnMode::FD) {
423
int accessMode = (flags & O_ACCMODE);
424
if (accessMode != O_WRONLY && accessMode != O_RDONLY &&
425
accessMode != O_RDWR) {
426
return -EINVAL;
427
}
428
429
// TODO: remove assert when all functionality is complete.
430
assert((flags & ~(O_CREAT | O_EXCL | O_DIRECTORY | O_TRUNC | O_APPEND |
431
O_RDWR | O_WRONLY | O_RDONLY | O_LARGEFILE | O_NOFOLLOW |
432
O_CLOEXEC | O_NONBLOCK)) == 0);
433
434
if (auto err = parsed.getError()) {
435
return err;
436
}
437
auto& [parent, childName] = parsed.getParentChild();
438
if (childName.size() > WASMFS_NAME_MAX) {
439
return -ENAMETOOLONG;
440
}
441
442
std::shared_ptr<File> child;
443
{
444
auto lockedParent = parent->locked();
445
child = lockedParent.getChild(std::string(childName));
446
// The requested node was not found.
447
if (!child) {
448
// If curr is the last element and the create flag is specified
449
// If O_DIRECTORY is also specified, still create a regular file:
450
// https://man7.org/linux/man-pages/man2/open.2.html#BUGS
451
if (!(flags & O_CREAT)) {
452
return -ENOENT;
453
}
454
455
// Inserting into an unlinked directory is not allowed.
456
if (!lockedParent.getParent()) {
457
return -ENOENT;
458
}
459
460
// Mask out everything except the permissions bits.
461
mode &= S_IALLUGO;
462
463
// If there is no explicitly provided backend, use the parent's backend.
464
if (!backend) {
465
backend = parent->getBackend();
466
}
467
468
// TODO: Check write permissions on the parent directory.
469
std::shared_ptr<File> created;
470
if (backend == parent->getBackend()) {
471
created = lockedParent.insertDataFile(std::string(childName), mode);
472
if (!created) {
473
// TODO Receive a specific error code, and report it here. For now,
474
// report a generic error.
475
return -EIO;
476
}
477
} else {
478
created = backend->createFile(mode);
479
if (!created) {
480
// TODO Receive a specific error code, and report it here. For now,
481
// report a generic error.
482
return -EIO;
483
}
484
[[maybe_unused]] bool mounted =
485
lockedParent.mountChild(std::string(childName), created);
486
assert(mounted);
487
}
488
// TODO: Check that the insert actually succeeds.
489
if (returnMode == OpenReturnMode::Nothing) {
490
return 0;
491
}
492
493
std::shared_ptr<OpenFileState> openFile;
494
if (auto err = OpenFileState::create(created, flags, openFile)) {
495
assert(err < 0);
496
return err;
497
}
498
return wasmFS.getFileTable().locked().addEntry(openFile);
499
}
500
}
501
502
if (auto link = child->dynCast<Symlink>()) {
503
if (flags & O_NOFOLLOW) {
504
return -ELOOP;
505
}
506
// TODO: The link dereference count starts back at 0 here. We could
507
// propagate it from the previous path parsing instead.
508
auto target = link->getTarget();
509
auto parsedLink = path::getFileFrom(parent, target);
510
if (auto err = parsedLink.getError()) {
511
return err;
512
}
513
child = parsedLink.getFile();
514
}
515
assert(!child->is<Symlink>());
516
517
// Return an error if the file exists and O_CREAT and O_EXCL are specified.
518
if ((flags & O_EXCL) && (flags & O_CREAT)) {
519
return -EEXIST;
520
}
521
522
if (child->is<Directory>() && (accessMode != O_RDONLY || (flags & O_CREAT))) {
523
return -EISDIR;
524
}
525
526
// Check user permissions.
527
auto fileMode = child->locked().getMode();
528
if ((accessMode == O_RDONLY || accessMode == O_RDWR) &&
529
!(fileMode & WASMFS_PERM_READ)) {
530
return -EACCES;
531
}
532
if ((accessMode == O_WRONLY || accessMode == O_RDWR) &&
533
!(fileMode & WASMFS_PERM_WRITE)) {
534
return -EACCES;
535
}
536
537
// Fail if O_DIRECTORY is specified and pathname is not a directory
538
if (flags & O_DIRECTORY && !child->is<Directory>()) {
539
return -ENOTDIR;
540
}
541
542
// Note that we open the file before truncating it because some backends may
543
// truncate opened files more efficiently (e.g. OPFS).
544
std::shared_ptr<OpenFileState> openFile;
545
if (auto err = OpenFileState::create(child, flags, openFile)) {
546
assert(err < 0);
547
return err;
548
}
549
550
// If O_TRUNC, truncate the file if possible.
551
if (flags & O_TRUNC) {
552
if (!child->is<DataFile>()) {
553
return -EISDIR;
554
}
555
if ((fileMode & WASMFS_PERM_WRITE) == 0) {
556
return -EACCES;
557
}
558
// Try to truncate the file, continuing silently if we cannot.
559
(void)child->cast<DataFile>()->locked().setSize(0);
560
}
561
562
return wasmFS.getFileTable().locked().addEntry(openFile);
563
}
564
565
// This function is exposed to users and allows users to create a file in a
566
// specific backend. An fd to an open file is returned.
567
int wasmfs_create_file(char* pathname, mode_t mode, backend_t backend) {
568
static_assert(std::is_same_v<decltype(doOpen(0, 0, 0, 0)), unsigned int>,
569
"unexpected conversion from result of doOpen to int");
570
return doOpen(
571
path::parseParent((char*)pathname), O_CREAT | O_EXCL, mode, backend);
572
}
573
574
// TODO: Test this with non-AT_FDCWD values.
575
int __syscall_openat(int dirfd, intptr_t path, int flags, ...) {
576
mode_t mode = 0;
577
va_list v1;
578
va_start(v1, flags);
579
mode = va_arg(v1, int);
580
va_end(v1);
581
582
return doOpen(path::parseParent((char*)path, dirfd), flags, mode);
583
}
584
585
int __syscall_mknodat(int dirfd, intptr_t path, int mode, int dev) {
586
assert(dev == 0); // TODO: support special devices
587
if (mode & S_IFDIR) {
588
return -EINVAL;
589
}
590
if (mode & S_IFIFO) {
591
return -EPERM;
592
}
593
return doOpen(path::parseParent((char*)path, dirfd),
594
O_CREAT | O_EXCL,
595
mode,
596
NullBackend,
597
OpenReturnMode::Nothing);
598
}
599
600
static int
601
doMkdir(path::ParsedParent parsed, int mode, backend_t backend = NullBackend) {
602
if (auto err = parsed.getError()) {
603
return err;
604
}
605
auto& [parent, childNameView] = parsed.getParentChild();
606
std::string childName(childNameView);
607
auto lockedParent = parent->locked();
608
609
if (childName.size() > WASMFS_NAME_MAX) {
610
return -ENAMETOOLONG;
611
}
612
613
// Check if the requested directory already exists.
614
if (lockedParent.getChild(childName)) {
615
return -EEXIST;
616
}
617
618
// Mask rwx permissions for user, group and others, and the sticky bit.
619
// This prevents users from entering S_IFREG for example.
620
// https://www.gnu.org/software/libc/manual/html_node/Permission-Bits.html
621
mode &= S_IRWXUGO | S_ISVTX;
622
623
if (!(lockedParent.getMode() & WASMFS_PERM_WRITE)) {
624
return -EACCES;
625
}
626
627
// By default, the backend that the directory is created in is the same as
628
// the parent directory. However, if a backend is passed as a parameter,
629
// then that backend is used.
630
if (!backend) {
631
backend = parent->getBackend();
632
}
633
634
if (backend == parent->getBackend()) {
635
if (!lockedParent.insertDirectory(childName, mode)) {
636
// TODO Receive a specific error code, and report it here. For now, report
637
// a generic error.
638
return -EIO;
639
}
640
} else {
641
auto created = backend->createDirectory(mode);
642
if (!created) {
643
// TODO Receive a specific error code, and report it here. For now, report
644
// a generic error.
645
return -EIO;
646
}
647
[[maybe_unused]] bool mounted = lockedParent.mountChild(childName, created);
648
assert(mounted);
649
}
650
651
// TODO: Check that the insertion is successful.
652
653
return 0;
654
}
655
656
// This function is exposed to users and allows users to specify a particular
657
// backend that a directory should be created within.
658
int wasmfs_create_directory(char* path, int mode, backend_t backend) {
659
static_assert(std::is_same_v<decltype(doMkdir(0, 0, 0)), int>,
660
"unexpected conversion from result of doMkdir to int");
661
return doMkdir(path::parseParent(path), mode, backend);
662
}
663
664
// TODO: Test this.
665
int __syscall_mkdirat(int dirfd, intptr_t path, int mode) {
666
return doMkdir(path::parseParent((char*)path, dirfd), mode);
667
}
668
669
__wasi_errno_t __wasi_fd_seek(__wasi_fd_t fd,
670
__wasi_filedelta_t offset,
671
__wasi_whence_t whence,
672
__wasi_filesize_t* newoffset) {
673
auto openFile = wasmFS.getFileTable().locked().getEntry(fd);
674
if (!openFile) {
675
return __WASI_ERRNO_BADF;
676
}
677
auto lockedOpenFile = openFile->locked();
678
679
if (!lockedOpenFile.getFile()->isSeekable()) {
680
return __WASI_ERRNO_SPIPE;
681
}
682
683
off_t position;
684
if (whence == SEEK_SET) {
685
position = offset;
686
} else if (whence == SEEK_CUR) {
687
position = lockedOpenFile.getPosition() + offset;
688
} else if (whence == SEEK_END) {
689
// Only the open file state is altered in seek. Locking the underlying
690
// data file here once is sufficient.
691
off_t size = lockedOpenFile.getFile()->locked().getSize();
692
if (size < 0) {
693
// Translate to WASI standard of positive return codes.
694
return -size;
695
}
696
position = size + offset;
697
} else {
698
return __WASI_ERRNO_INVAL;
699
}
700
701
if (position < 0) {
702
return __WASI_ERRNO_INVAL;
703
}
704
705
lockedOpenFile.setPosition(position);
706
707
if (newoffset) {
708
*newoffset = position;
709
}
710
711
return __WASI_ERRNO_SUCCESS;
712
}
713
714
static int doChdir(std::shared_ptr<File>& file) {
715
auto dir = file->dynCast<Directory>();
716
if (!dir) {
717
return -ENOTDIR;
718
}
719
wasmFS.setCWD(dir);
720
return 0;
721
}
722
723
int __syscall_chdir(intptr_t path) {
724
auto parsed = path::parseFile((char*)path);
725
if (auto err = parsed.getError()) {
726
return err;
727
}
728
return doChdir(parsed.getFile());
729
}
730
731
int __syscall_fchdir(int fd) {
732
auto openFile = wasmFS.getFileTable().locked().getEntry(fd);
733
if (!openFile) {
734
return -EBADF;
735
}
736
return doChdir(openFile->locked().getFile());
737
}
738
739
int __syscall_getcwd(intptr_t buf, size_t size) {
740
// Check if buf points to a bad address.
741
if (!buf && size > 0) {
742
return -EFAULT;
743
}
744
745
// Check if the size argument is zero and buf is not a null pointer.
746
if (buf && size == 0) {
747
return -EINVAL;
748
}
749
750
auto curr = wasmFS.getCWD();
751
752
std::string result = "";
753
754
while (curr != wasmFS.getRootDirectory()) {
755
auto parent = curr->locked().getParent();
756
// Check if the parent exists. The parent may not exist if the CWD or one
757
// of its ancestors has been unlinked.
758
if (!parent) {
759
return -ENOENT;
760
}
761
762
auto name = parent->locked().getName(curr);
763
result = '/' + name + result;
764
curr = parent;
765
}
766
767
// Check if the cwd is the root directory.
768
if (result.empty()) {
769
result = "/";
770
}
771
772
int len = result.length() + 1;
773
774
// Check if the size argument is less than the length of the absolute
775
// pathname of the working directory, including null terminator.
776
if (len > size) {
777
return -ERANGE;
778
}
779
780
// Return value is a null-terminated c string.
781
strcpy((char*)buf, result.c_str());
782
783
return len;
784
}
785
786
__wasi_errno_t __wasi_fd_fdstat_get(__wasi_fd_t fd, __wasi_fdstat_t* stat) {
787
// TODO: This is only partial implementation of __wasi_fd_fdstat_get. Enough
788
// to get __wasi_fd_is_valid working.
789
// There are other fields in the stat structure that we should really
790
// be filling in here.
791
auto openFile = wasmFS.getFileTable().locked().getEntry(fd);
792
if (!openFile) {
793
return __WASI_ERRNO_BADF;
794
}
795
796
if (openFile->locked().getFile()->is<Directory>()) {
797
stat->fs_filetype = __WASI_FILETYPE_DIRECTORY;
798
} else {
799
stat->fs_filetype = __WASI_FILETYPE_REGULAR_FILE;
800
}
801
return __WASI_ERRNO_SUCCESS;
802
}
803
804
// TODO: Test this with non-AT_FDCWD values.
805
int __syscall_unlinkat(int dirfd, intptr_t path, int flags) {
806
if (flags & ~AT_REMOVEDIR) {
807
// TODO: Test this case.
808
return -EINVAL;
809
}
810
// It is invalid for rmdir paths to end in ".", but we need to distinguish
811
// this case from the case of `parseParent` returning (root, '.') when parsing
812
// "/", so we need to find the invalid "/." manually.
813
if (flags == AT_REMOVEDIR) {
814
std::string_view p((char*)path);
815
// Ignore trailing '/'.
816
while (!p.empty() && p.back() == '/') {
817
p.remove_suffix(1);
818
}
819
if (p.size() >= 2 && p.substr(p.size() - 2) == std::string_view("/.")) {
820
return -EINVAL;
821
}
822
}
823
auto parsed = path::parseParent((char*)path, dirfd);
824
if (auto err = parsed.getError()) {
825
return err;
826
}
827
auto& [parent, childNameView] = parsed.getParentChild();
828
std::string childName(childNameView);
829
auto lockedParent = parent->locked();
830
auto file = lockedParent.getChild(childName);
831
if (!file) {
832
return -ENOENT;
833
}
834
// Disallow removing the root directory, even if it is empty.
835
if (file == wasmFS.getRootDirectory()) {
836
return -EBUSY;
837
}
838
839
auto lockedFile = file->locked();
840
if (auto dir = file->dynCast<Directory>()) {
841
if (flags != AT_REMOVEDIR) {
842
return -EISDIR;
843
}
844
// A directory can only be removed if it has no entries.
845
if (dir->locked().getNumEntries() > 0) {
846
return -ENOTEMPTY;
847
}
848
} else {
849
// A normal file or symlink.
850
if (flags == AT_REMOVEDIR) {
851
return -ENOTDIR;
852
}
853
}
854
855
// Cannot unlink/rmdir if the parent dir doesn't have write permissions.
856
if (!(lockedParent.getMode() & WASMFS_PERM_WRITE)) {
857
return -EACCES;
858
}
859
860
// Input is valid, perform the unlink.
861
return lockedParent.removeChild(childName);
862
}
863
864
int __syscall_rmdir(intptr_t path) {
865
return __syscall_unlinkat(AT_FDCWD, path, AT_REMOVEDIR);
866
}
867
868
// wasmfs_unmount is similar to __syscall_unlinkat, but assumes AT_REMOVEDIR is
869
// true and will only unlink mountpoints (Empty and nonempty).
870
int wasmfs_unmount(const char* path) {
871
auto parsed = path::parseParent(path, AT_FDCWD);
872
if (auto err = parsed.getError()) {
873
return err;
874
}
875
auto& [parent, childNameView] = parsed.getParentChild();
876
std::string childName(childNameView);
877
auto lockedParent = parent->locked();
878
auto file = lockedParent.getChild(childName);
879
if (!file) {
880
return -ENOENT;
881
}
882
// Disallow removing the root directory, even if it is empty.
883
if (file == wasmFS.getRootDirectory()) {
884
return -EBUSY;
885
}
886
887
if (!file->dynCast<Directory>()) {
888
// A normal file or symlink.
889
return -ENOTDIR;
890
}
891
892
if (parent->getBackend() == file->getBackend()) {
893
// The child is not a valid mountpoint.
894
return -EINVAL;
895
}
896
897
// Input is valid, perform the unlink.
898
return lockedParent.removeChild(childName);
899
}
900
901
int __syscall_getdents64(int fd, intptr_t dirp, size_t count) {
902
dirent* result = (dirent*)dirp;
903
904
// Check if the result buffer is too small.
905
if (count / sizeof(dirent) == 0) {
906
return -EINVAL;
907
}
908
909
auto openFile = wasmFS.getFileTable().locked().getEntry(fd);
910
if (!openFile) {
911
return -EBADF;
912
}
913
auto lockedOpenFile = openFile->locked();
914
915
auto dir = lockedOpenFile.getFile()->dynCast<Directory>();
916
if (!dir) {
917
return -ENOTDIR;
918
}
919
auto lockedDir = dir->locked();
920
921
// A directory's position corresponds to the index in its entries vector.
922
int index = lockedOpenFile.getPosition();
923
924
// If this directory has been unlinked and has no parent, then it is
925
// completely empty.
926
auto parent = lockedDir.getParent();
927
if (!parent) {
928
return 0;
929
}
930
931
off_t bytesRead = 0;
932
const auto& dirents = openFile->dirents;
933
for (; index < dirents.size() && bytesRead + sizeof(dirent) <= count;
934
index++) {
935
const auto& entry = dirents[index];
936
result->d_ino = entry.ino;
937
result->d_off = index + 1;
938
result->d_reclen = sizeof(dirent);
939
switch (entry.kind) {
940
case File::UnknownKind:
941
result->d_type = DT_UNKNOWN;
942
break;
943
case File::DataFileKind:
944
result->d_type = DT_REG;
945
break;
946
case File::DirectoryKind:
947
result->d_type = DT_DIR;
948
break;
949
case File::SymlinkKind:
950
result->d_type = DT_LNK;
951
break;
952
default:
953
result->d_type = DT_UNKNOWN;
954
break;
955
}
956
assert(entry.name.size() + 1 <= sizeof(result->d_name));
957
strcpy(result->d_name, entry.name.c_str());
958
++result;
959
bytesRead += sizeof(dirent);
960
}
961
962
// Update position
963
lockedOpenFile.setPosition(index);
964
965
return bytesRead;
966
}
967
968
// TODO: Test this with non-AT_FDCWD values.
969
int __syscall_renameat(int olddirfd,
970
intptr_t oldpath,
971
int newdirfd,
972
intptr_t newpath) {
973
// Rename is the only syscall that needs to (or is allowed to) acquire locks
974
// on two directories at once. It requires locks on both the old and new
975
// parent directories to ensure that the moved file can be atomically removed
976
// from the old directory and added to the new directory without something
977
// changing that would prevent the move.
978
//
979
// To prevent deadlock in the case of simultaneous renames, serialize renames
980
// with an additional global lock.
981
static std::mutex renameMutex;
982
std::lock_guard<std::mutex> renameLock(renameMutex);
983
984
// Get the old directory.
985
auto parsedOld = path::parseParent((char*)oldpath, olddirfd);
986
if (auto err = parsedOld.getError()) {
987
return err;
988
}
989
auto& [oldParent, oldFileNameView] = parsedOld.getParentChild();
990
std::string oldFileName(oldFileNameView);
991
992
// Get the new directory.
993
auto parsedNew = path::parseParent((char*)newpath, newdirfd);
994
if (auto err = parsedNew.getError()) {
995
return err;
996
}
997
auto& [newParent, newFileNameView] = parsedNew.getParentChild();
998
std::string newFileName(newFileNameView);
999
1000
if (newFileNameView.size() > WASMFS_NAME_MAX) {
1001
return -ENAMETOOLONG;
1002
}
1003
1004
// Lock both directories.
1005
auto lockedOldParent = oldParent->locked();
1006
auto lockedNewParent = newParent->locked();
1007
1008
// Get the source and destination files.
1009
auto oldFile = lockedOldParent.getChild(oldFileName);
1010
auto newFile = lockedNewParent.getChild(newFileName);
1011
1012
if (!oldFile) {
1013
return -ENOENT;
1014
}
1015
1016
// If the source and destination are the same, do nothing.
1017
if (oldFile == newFile) {
1018
return 0;
1019
}
1020
1021
// Never allow renaming or overwriting the root.
1022
auto root = wasmFS.getRootDirectory();
1023
if (oldFile == root || newFile == root) {
1024
return -EBUSY;
1025
}
1026
1027
// Cannot modify either directory without write permissions.
1028
if (!(lockedOldParent.getMode() & WASMFS_PERM_WRITE) ||
1029
!(lockedNewParent.getMode() & WASMFS_PERM_WRITE)) {
1030
return -EACCES;
1031
}
1032
1033
// Both parents must have the same backend.
1034
if (oldParent->getBackend() != newParent->getBackend()) {
1035
return -EXDEV;
1036
}
1037
1038
// Check that oldDir is not an ancestor of newDir.
1039
for (auto curr = newParent; curr != root; curr = curr->locked().getParent()) {
1040
if (curr == oldFile) {
1041
return -EINVAL;
1042
}
1043
}
1044
1045
// The new file will be removed if it already exists.
1046
if (newFile) {
1047
if (auto newDir = newFile->dynCast<Directory>()) {
1048
// Cannot overwrite a directory with a non-directory.
1049
auto oldDir = oldFile->dynCast<Directory>();
1050
if (!oldDir) {
1051
return -EISDIR;
1052
}
1053
// Cannot overwrite a non-empty directory.
1054
if (newDir->locked().getNumEntries() > 0) {
1055
return -ENOTEMPTY;
1056
}
1057
} else {
1058
// Cannot overwrite a non-directory with a directory.
1059
if (oldFile->is<Directory>()) {
1060
return -ENOTDIR;
1061
}
1062
}
1063
}
1064
1065
// Perform the move.
1066
if (auto err = lockedNewParent.insertMove(newFileName, oldFile)) {
1067
assert(err < 0);
1068
return err;
1069
}
1070
return 0;
1071
}
1072
1073
// TODO: Test this with non-AT_FDCWD values.
1074
int __syscall_symlinkat(intptr_t target, int newdirfd, intptr_t linkpath) {
1075
auto parsed = path::parseParent((char*)linkpath, newdirfd);
1076
if (auto err = parsed.getError()) {
1077
return err;
1078
}
1079
auto& [parent, childNameView] = parsed.getParentChild();
1080
if (childNameView.size() > WASMFS_NAME_MAX) {
1081
return -ENAMETOOLONG;
1082
}
1083
auto lockedParent = parent->locked();
1084
std::string childName(childNameView);
1085
if (lockedParent.getChild(childName)) {
1086
return -EEXIST;
1087
}
1088
if (!lockedParent.insertSymlink(childName, (char*)target)) {
1089
return -EPERM;
1090
}
1091
return 0;
1092
}
1093
1094
// TODO: Test this with non-AT_FDCWD values.
1095
int __syscall_readlinkat(int dirfd,
1096
intptr_t path,
1097
intptr_t buf,
1098
size_t bufsize) {
1099
// TODO: Handle empty paths.
1100
auto parsed = path::parseFile((char*)path, dirfd, path::NoFollowLinks);
1101
if (auto err = parsed.getError()) {
1102
return err;
1103
}
1104
auto link = parsed.getFile()->dynCast<Symlink>();
1105
if (!link) {
1106
return -EINVAL;
1107
}
1108
const auto& target = link->getTarget();
1109
auto bytes = std::min((size_t)bufsize, target.size());
1110
memcpy((char*)buf, target.c_str(), bytes);
1111
return bytes;
1112
}
1113
1114
static double timespec_to_ms(timespec ts) {
1115
if (ts.tv_nsec == UTIME_OMIT) {
1116
return INFINITY;
1117
}
1118
if (ts.tv_nsec == UTIME_NOW) {
1119
return emscripten_date_now();
1120
}
1121
return double(ts.tv_sec) * 1000 + double(ts.tv_nsec) / (1000 * 1000);
1122
}
1123
1124
// TODO: Test this with non-AT_FDCWD values.
1125
int __syscall_utimensat(int dirFD, intptr_t path_, intptr_t times_, int flags) {
1126
const char* path = (const char*)path_;
1127
const struct timespec* times = (const struct timespec*)times_;
1128
if (flags & ~AT_SYMLINK_NOFOLLOW) {
1129
// TODO: Test this case.
1130
return -EINVAL;
1131
}
1132
1133
// Add AT_EMPTY_PATH as Linux (and so, musl, and us) has a nonstandard
1134
// behavior in which an empty path means to operate on whatever is in dirFD
1135
// (directory or not), which is exactly the behavior of AT_EMPTY_PATH (but
1136
// without passing that in). See "C library/kernel ABI differences" in
1137
// https://man7.org/linux/man-pages/man2/utimensat.2.html
1138
//
1139
// TODO: Handle AT_SYMLINK_NOFOLLOW once we traverse symlinks correctly.
1140
auto parsed = path::getFileAt(dirFD, path, flags | AT_EMPTY_PATH);
1141
if (auto err = parsed.getError()) {
1142
return err;
1143
}
1144
1145
// TODO: Handle tv_nsec being UTIME_NOW or UTIME_OMIT.
1146
// TODO: Check for write access to the file (see man page for specifics).
1147
double aTime, mTime;
1148
1149
if (times == nullptr) {
1150
aTime = mTime = emscripten_date_now();
1151
} else {
1152
aTime = timespec_to_ms(times[0]);
1153
mTime = timespec_to_ms(times[1]);
1154
}
1155
1156
auto locked = parsed.getFile()->locked();
1157
if (aTime != INFINITY) {
1158
locked.setATime(aTime);
1159
}
1160
if (mTime != INFINITY) {
1161
locked.setMTime(mTime);
1162
}
1163
1164
return 0;
1165
}
1166
1167
// TODO: Test this with non-AT_FDCWD values.
1168
int __syscall_fchmodat2(int dirfd, intptr_t path, int mode, int flags) {
1169
if (flags & ~AT_SYMLINK_NOFOLLOW) {
1170
// TODO: Test this case.
1171
return -EINVAL;
1172
}
1173
auto parsed = path::getFileAt(dirfd, (char*)path, flags);
1174
if (auto err = parsed.getError()) {
1175
return err;
1176
}
1177
auto lockedFile = parsed.getFile()->locked();
1178
lockedFile.setMode(mode);
1179
// On POSIX, ctime is updated on metadata changes, like chmod.
1180
lockedFile.updateCTime();
1181
return 0;
1182
}
1183
1184
int __syscall_chmod(intptr_t path, int mode) {
1185
return __syscall_fchmodat2(AT_FDCWD, path, mode, 0);
1186
}
1187
1188
int __syscall_fchmod(int fd, int mode) {
1189
auto openFile = wasmFS.getFileTable().locked().getEntry(fd);
1190
if (!openFile) {
1191
return -EBADF;
1192
}
1193
auto lockedFile = openFile->locked().getFile()->locked();
1194
lockedFile.setMode(mode);
1195
lockedFile.updateCTime();
1196
return 0;
1197
}
1198
1199
int __syscall_fchownat(
1200
int dirfd, intptr_t path, int owner, int group, int flags) {
1201
// Only accept valid flags.
1202
if (flags & ~(AT_EMPTY_PATH | AT_SYMLINK_NOFOLLOW)) {
1203
// TODO: Test this case.
1204
return -EINVAL;
1205
}
1206
auto parsed = path::getFileAt(dirfd, (char*)path, flags);
1207
if (auto err = parsed.getError()) {
1208
return err;
1209
}
1210
1211
// Ignore the actual owner and group because we don't track those.
1212
// TODO: Update metadata time stamp.
1213
return 0;
1214
}
1215
1216
int __syscall_fchown32(int fd, int owner, int group) {
1217
return __syscall_fchownat(fd, (intptr_t) "", owner, group, AT_EMPTY_PATH);
1218
}
1219
1220
// TODO: Test this with non-AT_FDCWD values.
1221
int __syscall_faccessat(int dirfd, intptr_t path, int amode, int flags) {
1222
// The input must be F_OK (check for existence) or a combination of [RWX]_OK
1223
// flags.
1224
if (amode != F_OK && (amode & ~(R_OK | W_OK | X_OK))) {
1225
return -EINVAL;
1226
}
1227
if (flags & ~(AT_EACCESS | AT_SYMLINK_NOFOLLOW)) {
1228
// TODO: Test this case.
1229
return -EINVAL;
1230
}
1231
1232
// TODO: Handle AT_SYMLINK_NOFOLLOW once we traverse symlinks correctly.
1233
auto parsed = path::parseFile((char*)path, dirfd);
1234
if (auto err = parsed.getError()) {
1235
return err;
1236
}
1237
1238
if (amode != F_OK) {
1239
auto mode = parsed.getFile()->locked().getMode();
1240
if ((amode & R_OK) && !(mode & WASMFS_PERM_READ)) {
1241
return -EACCES;
1242
}
1243
if ((amode & W_OK) && !(mode & WASMFS_PERM_WRITE)) {
1244
return -EACCES;
1245
}
1246
if ((amode & X_OK) && !(mode & WASMFS_PERM_EXECUTE)) {
1247
return -EACCES;
1248
}
1249
}
1250
1251
return 0;
1252
}
1253
1254
static int doTruncate(std::shared_ptr<File>& file, off_t size) {
1255
auto dataFile = file->dynCast<DataFile>();
1256
1257
if (!dataFile) {
1258
return -EISDIR;
1259
}
1260
1261
auto locked = dataFile->locked();
1262
if (!(locked.getMode() & WASMFS_PERM_WRITE)) {
1263
return -EACCES;
1264
}
1265
1266
if (size < 0) {
1267
return -EINVAL;
1268
}
1269
1270
int ret = locked.setSize(size);
1271
assert(ret <= 0);
1272
return ret;
1273
}
1274
1275
int __syscall_truncate64(intptr_t path, off_t size) {
1276
auto parsed = path::parseFile((char*)path);
1277
if (auto err = parsed.getError()) {
1278
return err;
1279
}
1280
return doTruncate(parsed.getFile(), size);
1281
}
1282
1283
int __syscall_ftruncate64(int fd, off_t size) {
1284
auto openFile = wasmFS.getFileTable().locked().getEntry(fd);
1285
if (!openFile) {
1286
return -EBADF;
1287
}
1288
auto ret = doTruncate(openFile->locked().getFile(), size);
1289
// XXX It is not clear from the docs why ftruncate would differ from
1290
// truncate here. However, on Linux this definitely happens, and the old
1291
// FS matches that as well, so do the same here.
1292
if (ret == -EACCES) {
1293
ret = -EINVAL;
1294
}
1295
return ret;
1296
}
1297
1298
static bool isTTY(std::shared_ptr<File>& file) {
1299
// TODO: Full TTY support. For now, just see stdin/out/err as terminals and
1300
// nothing else.
1301
return file == SpecialFiles::getStdin() ||
1302
file == SpecialFiles::getStdout() || file == SpecialFiles::getStderr();
1303
}
1304
1305
int __syscall_ioctl(int fd, int request, ...) {
1306
auto openFile = wasmFS.getFileTable().locked().getEntry(fd);
1307
if (!openFile) {
1308
return -EBADF;
1309
}
1310
if (!isTTY(openFile->locked().getFile())) {
1311
return -ENOTTY;
1312
}
1313
// TODO: Full TTY support. For now this is limited, and matches the old FS.
1314
switch (request) {
1315
case TCGETA:
1316
case TCGETS:
1317
case TCSETA:
1318
case TCSETAW:
1319
case TCSETAF:
1320
case TCSETS:
1321
case TCSETSW:
1322
case TCSETSF:
1323
case TIOCGWINSZ:
1324
case TIOCSWINSZ: {
1325
// TTY operations that we do nothing for anyhow can just be ignored.
1326
return 0;
1327
}
1328
default: {
1329
return -EINVAL; // not supported
1330
}
1331
}
1332
}
1333
1334
int __syscall_pipe(intptr_t fd) {
1335
auto* fds = (__wasi_fd_t*)fd;
1336
1337
// Make a pipe: Two PipeFiles that share a single data source between them, so
1338
// that writing to one can be read in the other.
1339
//
1340
// No backend is needed here, so pass in nullptr for that.
1341
auto data = std::make_shared<PipeData>();
1342
auto reader = std::make_shared<PipeFile>(S_IRUGO, data);
1343
auto writer = std::make_shared<PipeFile>(S_IWUGO, data);
1344
1345
std::shared_ptr<OpenFileState> openReader, openWriter;
1346
(void)OpenFileState::create(reader, O_RDONLY, openReader);
1347
(void)OpenFileState::create(writer, O_WRONLY, openWriter);
1348
1349
auto fileTable = wasmFS.getFileTable().locked();
1350
fds[0] = fileTable.addEntry(openReader);
1351
fds[1] = fileTable.addEntry(openWriter);
1352
1353
return 0;
1354
}
1355
1356
// int poll(struct pollfd* fds, nfds_t nfds, int timeout);
1357
int __syscall_poll(intptr_t fds_, int nfds, int timeout) {
1358
struct pollfd* fds = (struct pollfd*)fds_;
1359
auto fileTable = wasmFS.getFileTable().locked();
1360
1361
// Process the list of FDs and compute their revents masks. Count the number
1362
// of nonzero such masks, which is our return value.
1363
int nonzero = 0;
1364
for (nfds_t i = 0; i < nfds; i++) {
1365
auto* pollfd = &fds[i];
1366
auto fd = pollfd->fd;
1367
if (fd < 0) {
1368
// Negative FDs are ignored in poll().
1369
pollfd->revents = 0;
1370
continue;
1371
}
1372
// Assume invalid, unless there is an open file.
1373
auto mask = POLLNVAL;
1374
auto openFile = fileTable.getEntry(fd);
1375
if (openFile) {
1376
mask = 0;
1377
auto flags = openFile->locked().getFlags();
1378
auto accessMode = flags & O_ACCMODE;
1379
auto readBit = pollfd->events & POLLOUT;
1380
if (readBit && (accessMode == O_WRONLY || accessMode == O_RDWR)) {
1381
mask |= readBit;
1382
}
1383
auto writeBit = pollfd->events & POLLIN;
1384
if (writeBit && (accessMode == O_RDONLY || accessMode == O_RDWR)) {
1385
// If there is data in the file, then there is also the ability to read.
1386
// TODO: Does this need to consider the position as well? That is, if
1387
// the position is at the end, we can't read from the current position
1388
// at least. If we update this, make sure the size isn't an error!
1389
if (openFile->locked().getFile()->locked().getSize() > 0) {
1390
mask |= writeBit;
1391
}
1392
}
1393
// TODO: get mask from File dynamically using a poll() hook?
1394
}
1395
// TODO: set the state based on the state of the other end of the pipe, for
1396
// pipes (POLLERR | POLLHUP)
1397
if (mask) {
1398
nonzero++;
1399
}
1400
pollfd->revents = mask;
1401
}
1402
// TODO: This should block based on the timeout. The old FS did not do so due
1403
// to web limitations, which we should perhaps revisit (especially with
1404
// pthreads and asyncify).
1405
return nonzero;
1406
}
1407
1408
int __syscall_fallocate(int fd, int mode, off_t offset, off_t len) {
1409
assert(mode == 0); // TODO, but other modes were never supported in the old FS
1410
1411
auto fileTable = wasmFS.getFileTable().locked();
1412
auto openFile = fileTable.getEntry(fd);
1413
if (!openFile) {
1414
return -EBADF;
1415
}
1416
1417
auto dataFile = openFile->locked().getFile()->dynCast<DataFile>();
1418
// TODO: support for symlinks.
1419
if (!dataFile) {
1420
return -ENODEV;
1421
}
1422
1423
auto locked = dataFile->locked();
1424
if (!(locked.getMode() & WASMFS_PERM_WRITE)) {
1425
return -EBADF;
1426
}
1427
1428
if (offset < 0 || len <= 0) {
1429
return -EINVAL;
1430
}
1431
1432
// TODO: We could only fill zeros for regions that were completely unused
1433
// before, which for a backend with sparse data storage could make a
1434
// difference. For that we'd need a new backend API.
1435
auto newNeededSize = offset + len;
1436
off_t size = locked.getSize();
1437
if (size < 0) {
1438
return size;
1439
}
1440
if (newNeededSize > size) {
1441
if (auto err = locked.setSize(newNeededSize)) {
1442
assert(err < 0);
1443
return err;
1444
}
1445
}
1446
1447
return 0;
1448
}
1449
1450
int __syscall_fcntl64(int fd, int cmd, ...) {
1451
auto fileTable = wasmFS.getFileTable().locked();
1452
auto openFile = fileTable.getEntry(fd);
1453
if (!openFile) {
1454
return -EBADF;
1455
}
1456
1457
switch (cmd) {
1458
case F_DUPFD: {
1459
int newfd;
1460
va_list v1;
1461
va_start(v1, cmd);
1462
newfd = va_arg(v1, int);
1463
va_end(v1);
1464
if (newfd < 0) {
1465
return -EINVAL;
1466
}
1467
1468
// Find the first available fd at arg or after.
1469
// TODO: Should we check for a limit on the max FD number, if we have one?
1470
while (1) {
1471
if (!fileTable.getEntry(newfd)) {
1472
(void)fileTable.setEntry(newfd, openFile);
1473
return newfd;
1474
}
1475
newfd++;
1476
}
1477
}
1478
case F_GETFD:
1479
case F_SETFD:
1480
// FD_CLOEXEC makes no sense for a single process.
1481
return 0;
1482
case F_GETFL:
1483
return openFile->locked().getFlags();
1484
case F_SETFL: {
1485
int flags;
1486
va_list v1;
1487
va_start(v1, cmd);
1488
flags = va_arg(v1, int);
1489
va_end(v1);
1490
// This syscall should ignore most flags.
1491
flags = flags & ~(O_RDONLY | O_WRONLY | O_RDWR | O_CREAT | O_EXCL |
1492
O_NOCTTY | O_TRUNC);
1493
// Also ignore this flag which musl always adds constantly, but does not
1494
// matter for us.
1495
flags = flags & ~O_LARGEFILE;
1496
// On linux only a few flags can be modified, and we support only a subset
1497
// of those. Error on anything else.
1498
auto supportedFlags = flags & O_APPEND;
1499
if (flags != supportedFlags) {
1500
return -EINVAL;
1501
}
1502
openFile->locked().setFlags(flags);
1503
return 0;
1504
}
1505
case F_GETLK: {
1506
// If these constants differ then we'd need a case for both.
1507
static_assert(F_GETLK == F_GETLK64);
1508
flock* data;
1509
va_list v1;
1510
va_start(v1, cmd);
1511
data = va_arg(v1, flock*);
1512
va_end(v1);
1513
// We're always unlocked for now, until we implement byte-range locks.
1514
data->l_type = F_UNLCK;
1515
return 0;
1516
}
1517
case F_SETLK:
1518
case F_SETLKW: {
1519
static_assert(F_SETLK == F_SETLK64);
1520
static_assert(F_SETLKW == F_SETLKW64);
1521
// Pretend that the locking is successful. These are process-level locks,
1522
// and Emscripten programs are a single process. If we supported linking a
1523
// filesystem between programs, we'd need to do more here.
1524
// See https://github.com/emscripten-core/emscripten/issues/23697
1525
return 0;
1526
}
1527
default: {
1528
// TODO: support any remaining cmds
1529
return -EINVAL;
1530
}
1531
}
1532
}
1533
1534
static int
1535
doStatFS(std::shared_ptr<File>& file, size_t size, struct statfs* buf) {
1536
if (size != sizeof(struct statfs)) {
1537
// We only know how to write to a standard statfs, not even a truncated one.
1538
return -EINVAL;
1539
}
1540
1541
// NOTE: None of the constants here are true. We're just returning safe and
1542
// sane values, that match the long-existing JS FS behavior (except for
1543
// the inode number, where we can do better).
1544
buf->f_type = 0;
1545
buf->f_bsize = 4096;
1546
buf->f_frsize = 4096;
1547
buf->f_blocks = 1000000;
1548
buf->f_bfree = 500000;
1549
buf->f_bavail = 500000;
1550
buf->f_files = file->getIno();
1551
buf->f_ffree = 1000000;
1552
buf->f_fsid = {0, 0};
1553
buf->f_flags = ST_NOSUID;
1554
buf->f_namelen = 255;
1555
return 0;
1556
}
1557
1558
int __syscall_statfs64(intptr_t path, size_t size, intptr_t buf) {
1559
auto parsed = path::parseFile((char*)path);
1560
if (auto err = parsed.getError()) {
1561
return err;
1562
}
1563
return doStatFS(parsed.getFile(), size, (struct statfs*)buf);
1564
}
1565
1566
int __syscall_fstatfs64(int fd, size_t size, intptr_t buf) {
1567
auto openFile = wasmFS.getFileTable().locked().getEntry(fd);
1568
if (!openFile) {
1569
return -EBADF;
1570
}
1571
return doStatFS(openFile->locked().getFile(), size, (struct statfs*)buf);
1572
}
1573
1574
int _mmap_js(size_t length,
1575
int prot,
1576
int flags,
1577
int fd,
1578
off_t offset,
1579
int* allocated,
1580
void** addr) {
1581
// PROT_EXEC is not supported (although we pretend to support the absence of
1582
// PROT_READ or PROT_WRITE).
1583
if ((prot & PROT_EXEC)) {
1584
return -EPERM;
1585
}
1586
1587
if (!length) {
1588
return -EINVAL;
1589
}
1590
1591
// One of MAP_PRIVATE, MAP_SHARED, or MAP_SHARED_VALIDATE must be used.
1592
int mapType = flags & MAP_TYPE;
1593
if (mapType != MAP_PRIVATE && mapType != MAP_SHARED &&
1594
mapType != MAP_SHARED_VALIDATE) {
1595
return -EINVAL;
1596
}
1597
1598
if (mapType == MAP_SHARED_VALIDATE) {
1599
WASMFS_UNREACHABLE("TODO: MAP_SHARED_VALIDATE");
1600
}
1601
1602
auto openFile = wasmFS.getFileTable().locked().getEntry(fd);
1603
if (!openFile) {
1604
return -EBADF;
1605
}
1606
1607
std::shared_ptr<DataFile> file;
1608
1609
// Keep the open file info locked only for as long as we need that.
1610
{
1611
auto lockedOpenFile = openFile->locked();
1612
1613
// Check permissions. We always need read permissions, since we need to read
1614
// the data in the file to map it.
1615
if ((lockedOpenFile.getFlags() & O_ACCMODE) == O_WRONLY) {
1616
return -EACCES;
1617
}
1618
1619
// According to the POSIX spec it is possible to write to a file opened in
1620
// read-only mode with MAP_PRIVATE flag, as all modifications will be
1621
// visible only in the memory of the current process.
1622
if ((prot & PROT_WRITE) != 0 && mapType != MAP_PRIVATE &&
1623
(lockedOpenFile.getFlags() & O_ACCMODE) != O_RDWR) {
1624
return -EACCES;
1625
}
1626
1627
file = lockedOpenFile.getFile()->dynCast<DataFile>();
1628
}
1629
1630
if (!file) {
1631
return -ENODEV;
1632
}
1633
1634
// TODO: On MAP_SHARED, install the mapping on the DataFile object itself so
1635
// that reads and writes can be redirected to the mapped region and so that
1636
// the mapping can correctly outlive the file being closed. This will require
1637
// changes to emscripten_mmap.c as well.
1638
1639
// Align to a wasm page size, as we expect in the future to get wasm
1640
// primitives to do this work, and those would presumably be aligned to a page
1641
// size. Aligning now avoids confusion later.
1642
uint8_t* ptr = (uint8_t*)emscripten_builtin_memalign(WASM_PAGE_SIZE, length);
1643
if (!ptr) {
1644
return -ENOMEM;
1645
}
1646
1647
auto nread = file->locked().read(ptr, length, offset);
1648
if (nread < 0) {
1649
// The read failed. Report the error, but first free the allocation.
1650
emscripten_builtin_free(ptr);
1651
return nread;
1652
}
1653
1654
// From here on, we have succeeded, and can mark the allocation as having
1655
// occurred (which means that the caller has the responsibility to free it).
1656
*allocated = true;
1657
*addr = (void*)ptr;
1658
1659
// The read must be of a valid amount, or we have had an internal logic error.
1660
assert(nread <= length);
1661
1662
// mmap clears any extra bytes after the data itself.
1663
memset(ptr + nread, 0, length - nread);
1664
1665
return 0;
1666
}
1667
1668
int _msync_js(
1669
intptr_t addr, size_t length, int prot, int flags, int fd, off_t offset) {
1670
// TODO: This is not correct! Mappings should be associated with files, not
1671
// fds. Only need to sync if shared and writes are allowed.
1672
int mapType = flags & MAP_TYPE;
1673
if (mapType == MAP_SHARED && (prot & PROT_WRITE)) {
1674
__wasi_ciovec_t iovec;
1675
iovec.buf = (uint8_t*)addr;
1676
iovec.buf_len = length;
1677
__wasi_size_t nwritten;
1678
// Translate from WASI positive error codes to negative error codes.
1679
return -__wasi_fd_pwrite(fd, &iovec, 1, offset, &nwritten);
1680
}
1681
return 0;
1682
}
1683
1684
int _munmap_js(
1685
intptr_t addr, size_t length, int prot, int flags, int fd, off_t offset) {
1686
// TODO: This is not correct! Mappings should be associated with files, not
1687
// fds.
1688
// TODO: Syncing should probably be handled in __syscall_munmap instead.
1689
return _msync_js(addr, length, prot, flags, fd, offset);
1690
}
1691
1692
// Stubs (at least for now)
1693
1694
int __syscall_accept4(int sockfd,
1695
intptr_t addr,
1696
intptr_t addrlen,
1697
int flags,
1698
int dummy1,
1699
int dummy2) {
1700
return -ENOSYS;
1701
}
1702
1703
int __syscall_bind(
1704
int sockfd, intptr_t addr, size_t alen, int dummy, int dummy2, int dummy3) {
1705
return -ENOSYS;
1706
}
1707
1708
int __syscall_connect(
1709
int sockfd, intptr_t addr, size_t len, int dummy, int dummy2, int dummy3) {
1710
return -ENOSYS;
1711
}
1712
1713
int __syscall_socket(
1714
int domain, int type, int protocol, int dummy1, int dummy2, int dummy3) {
1715
return -ENOSYS;
1716
}
1717
1718
int __syscall_listen(
1719
int sockfd, int backlog, int dummy1, int dummy2, int dummy3, int dummy4) {
1720
return -ENOSYS;
1721
}
1722
1723
int __syscall_getsockopt(int sockfd,
1724
int level,
1725
int optname,
1726
intptr_t optval,
1727
intptr_t optlen,
1728
int dummy) {
1729
return -ENOSYS;
1730
}
1731
1732
int __syscall_getsockname(
1733
int sockfd, intptr_t addr, intptr_t len, int dummy, int dummy2, int dummy3) {
1734
return -ENOSYS;
1735
}
1736
1737
int __syscall_getpeername(
1738
int sockfd, intptr_t addr, intptr_t len, int dummy, int dummy2, int dummy3) {
1739
return -ENOSYS;
1740
}
1741
1742
int __syscall_sendto(
1743
int sockfd, intptr_t msg, size_t len, int flags, intptr_t addr, size_t alen) {
1744
return -ENOSYS;
1745
}
1746
1747
int __syscall_sendmsg(
1748
int sockfd, intptr_t msg, int flags, intptr_t addr, size_t alen, int dummy) {
1749
return -ENOSYS;
1750
}
1751
1752
int __syscall_recvfrom(int sockfd,
1753
intptr_t msg,
1754
size_t len,
1755
int flags,
1756
intptr_t addr,
1757
intptr_t alen) {
1758
return -ENOSYS;
1759
}
1760
1761
int __syscall_recvmsg(
1762
int sockfd, intptr_t msg, int flags, int dummy, int dummy2, int dummy3) {
1763
return -ENOSYS;
1764
}
1765
1766
int __syscall_fadvise64(int fd, off_t offset, off_t length, int advice) {
1767
// Advice is currently ignored. TODO some backends might use it
1768
return 0;
1769
}
1770
1771
} // extern "C"
1772
1773