Repository: tetratelabs/wazero
Pull request #1234 (2023-03-13 23:49:02)

gojs: fixes symlink
```
$ wazero run -mount=/:/ --experimental-workdir=$(go env GOROOT)/src/os os.wasm -test.run '^TestSymlink|^TestLongSymlink|^TestStatRelativeSymlink|^TestLongSymlink'
PASS
```


Patch:
diff --git a/internal/gojs/builtin.go b/internal/gojs/builtin.go
index 4187f2ff..06fe4bd5 100644
--- a/internal/gojs/builtin.go
+++ b/internal/gojs/builtin.go
@@ -55,7 +55,7 @@ var (
 		addFunction(custom.NameProcessGetgid, returnZero{}).           // syscall.Getgid in syscall_js.go
 		addFunction(custom.NameProcessGeteuid, returnZero{}).          // syscall.Geteuid in syscall_js.go
 		addFunction(custom.NameProcessGetgroups, returnSliceOfZero{}). // syscall.Getgroups in syscall_js.go
-		addFunction(custom.NameProcessUmask, returnArg0{})             // syscall.Umask in syscall_js.go
+		addFunction(custom.NameProcessUmask, processUmask{})           // syscall.Umask in syscall_js.go
 
 	// uint8ArrayConstructor = js.Global().Get("Uint8Array")
 	//	// fs_js.go, rand_js.go, roundtrip_js.go init
diff --git a/internal/gojs/custom/fs.go b/internal/gojs/custom/fs.go
index f8041f8a..fbdd8eea 100644
--- a/internal/gojs/custom/fs.go
+++ b/internal/gojs/custom/fs.go
@@ -1,5 +1,7 @@
 package custom
 
+import "io/fs"
+
 const (
 	NameFs          = "fs"
 	NameFsOpen      = "open"
@@ -153,3 +155,94 @@ var FsNameSection = map[string]*Names{
 		ResultNames: []string{"err", "ok"},
 	},
 }
+
+// mode constants from syscall_js.go
+const (
+	S_IFSOCK = uint32(0o000140000)
+	S_IFLNK  = uint32(0o000120000)
+	S_IFREG  = uint32(0o000100000)
+	S_IFBLK  = uint32(0o000060000)
+	S_IFDIR  = uint32(0o000040000)
+	S_IFCHR  = uint32(0o000020000)
+	S_IFIFO  = uint32(0o000010000)
+
+	S_ISUID = uint32(0o004000)
+	S_ISGID = uint32(0o002000)
+	S_ISVTX = uint32(0o001000)
+)
+
+// ToJsMode is required because the mode property read in `GOOS=js` is
+// incompatible with normal go. Particularly the directory flag isn't the same.
+func ToJsMode(fm fs.FileMode) (jsMode uint32) {
+	switch {
+	case fm.IsRegular():
+		jsMode = S_IFREG
+	case fm.IsDir():
+		jsMode = S_IFDIR
+	case fm&fs.ModeSymlink != 0:
+		jsMode = S_IFLNK
+	case fm&fs.ModeDevice != 0:
+		// Unlike ModeDevice and ModeCharDevice, S_IFCHR and S_IFBLK are set
+		// mutually exclusively.
+		if fm&fs.ModeCharDevice != 0 {
+			jsMode = S_IFCHR
+		} else {
+			jsMode = S_IFBLK
+		}
+	case fm&fs.ModeNamedPipe != 0:
+		jsMode = S_IFIFO
+	case fm&fs.ModeSocket != 0:
+		jsMode = S_IFSOCK
+	default: // unknown
+		jsMode = 0
+	}
+
+	jsMode |= uint32(fm & fs.ModePerm)
+
+	if fm&fs.ModeSetgid != 0 {
+		jsMode |= S_ISGID
+	}
+	if fm&fs.ModeSetuid != 0 {
+		jsMode |= S_ISUID
+	}
+	if fm&fs.ModeSticky != 0 {
+		jsMode |= S_ISVTX
+	}
+	return
+}
+
+// FromJsMode is required because the mode property read in `GOOS=js` is
+// incompatible with normal go. Particularly the directory flag isn't the same.
+func FromJsMode(jsMode, umask uint32) (fm fs.FileMode) {
+	switch {
+	case jsMode&S_IFREG != 0: // zero
+	case jsMode&S_IFDIR != 0:
+		fm = fs.ModeDir
+	case jsMode&S_IFLNK != 0:
+		fm = fs.ModeSymlink
+	case jsMode&S_IFCHR != 0:
+		fm = fs.ModeDevice | fs.ModeCharDevice
+	case jsMode&S_IFBLK != 0:
+		fm = fs.ModeDevice
+	case jsMode&S_IFIFO != 0:
+		fm = fs.ModeNamedPipe
+	case jsMode&S_IFSOCK != 0:
+		fm = fs.ModeSocket
+	default: // unknown
+		fm = 0
+	}
+
+	fm |= fs.FileMode(jsMode) & fs.ModePerm
+
+	if jsMode&S_ISGID != 0 {
+		fm |= fs.ModeSetgid
+	}
+	if jsMode&S_ISUID != 0 {
+		fm |= fs.ModeSetuid
+	}
+	if jsMode&S_ISVTX != 0 {
+		fm |= fs.ModeSticky
+	}
+	fm &= ^(fs.FileMode(umask))
+	return
+}
diff --git a/internal/gojs/fs.go b/internal/gojs/fs.go
index 4f545348..c57d0c67 100644
--- a/internal/gojs/fs.go
+++ b/internal/gojs/fs.go
@@ -101,12 +101,12 @@ type jsfsOpen struct{}
 func (jsfsOpen) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
 	path := resolvePath(ctx, args[0].(string))
 	flags := toUint64(args[1]) // flags are derived from constants like oWRONLY
-	perm := goos.ValueToUint32(args[2])
+	perm := getPerm(ctx, goos.ValueToUint32(args[2]))
 	callback := args[3].(funcWrapper)
 
 	fsc := mod.(*wasm.CallContext).Sys.FS()
 
-	fd, err := fsc.OpenFile(fsc.RootFS(), path, int(flags), fs.FileMode(perm))
+	fd, err := fsc.OpenFile(fsc.RootFS(), path, int(flags), perm)
 
 	return callback.invoke(ctx, mod, goos.RefJsfs, err, fd) // note: error first
 }
@@ -175,21 +175,6 @@ func (jsfsFstat) invoke(ctx context.Context, mod api.Module, args ...interface{}
 	return callback.invoke(ctx, mod, goos.RefJsfs, err, fstat) // note: error first
 }
 
-// mode constants from syscall_js.go
-const (
-	S_IFSOCK = uint32(0o000140000)
-	S_IFLNK  = uint32(0o000120000)
-	S_IFREG  = uint32(0o000100000)
-	S_IFBLK  = uint32(0o000060000)
-	S_IFDIR  = uint32(0o000040000)
-	S_IFCHR  = uint32(0o000020000)
-	S_IFIFO  = uint32(0o000010000)
-
-	S_ISUID = uint32(0o004000)
-	S_ISGID = uint32(0o002000)
-	S_ISVTX = uint32(0o001000)
-)
-
 // syscallFstat is like syscall.Fstat
 func syscallFstat(fsc *internalsys.FSContext, fd uint32) (*jsSt, error) {
 	f, ok := fsc.LookupFile(fd)
@@ -209,7 +194,7 @@ func newJsSt(st *platform.Stat_t) *jsSt {
 	ret.isDir = st.Mode.IsDir()
 	ret.dev = st.Dev
 	ret.ino = st.Ino
-	ret.mode = getJsMode(st.Mode)
+	ret.mode = custom.ToJsMode(st.Mode)
 	ret.nlink = uint32(st.Nlink)
 	ret.size = st.Size
 	ret.atimeMs = st.Atim / 1e6
@@ -218,46 +203,6 @@ func newJsSt(st *platform.Stat_t) *jsSt {
 	return ret
 }
 
-// getJsMode is required because the mode property read in `GOOS=js` is
-// incompatible with normal go. Particularly the directory flag isn't the same.
-func getJsMode(fm fs.FileMode) (jsMode uint32) {
-	switch {
-	case fm.IsRegular():
-		jsMode = S_IFREG
-	case fm.IsDir():
-		jsMode = S_IFDIR
-	case fm&fs.ModeSymlink != 0:
-		jsMode = S_IFLNK
-	case fm&fs.ModeDevice != 0:
-		// Unlike ModeDevice and ModeCharDevice, S_IFCHR and S_IFBLK are set
-		// mutually exclusively.
-		if fm&fs.ModeCharDevice != 0 {
-			jsMode = S_IFCHR
-		} else {
-			jsMode = S_IFBLK
-		}
-	case fm&fs.ModeNamedPipe != 0:
-		jsMode = S_IFIFO
-	case fm&fs.ModeSocket != 0:
-		jsMode = S_IFSOCK
-	default: // unknown
-		jsMode = 0
-	}
-
-	jsMode |= uint32(fm & fs.ModePerm)
-
-	if fm&fs.ModeSetgid != 0 {
-		jsMode |= S_ISGID
-	}
-	if fm&fs.ModeSetuid != 0 {
-		jsMode |= S_ISUID
-	}
-	if fm&fs.ModeSticky != 0 {
-		jsMode |= S_ISVTX
-	}
-	return
-}
-
 // jsfsClose implements jsFn for syscall.Close
 type jsfsClose struct{}
 
@@ -415,13 +360,6 @@ func (returnSliceOfZero) invoke(context.Context, api.Module, ...interface{}) (in
 	return &objectArray{slice: []interface{}{goos.RefValueZero}}, nil
 }
 
-// returnArg0 implements jsFn
-type returnArg0 struct{}
-
-func (returnArg0) invoke(_ context.Context, _ api.Module, args ...interface{}) (interface{}, error) {
-	return args[0], nil
-}
-
 // processCwd implements jsFn for fs.Open syscall.Getcwd in fs_js.go
 type processCwd struct{}
 
@@ -445,6 +383,19 @@ func (processChdir) invoke(ctx context.Context, mod api.Module, args ...interfac
 	}
 }
 
+// processUmask implements jsFn for fs.Open syscall.Umask in fs_js.go
+type processUmask struct{}
+
+func (processUmask) invoke(ctx context.Context, _ api.Module, args ...interface{}) (interface{}, error) {
+	mask := goos.ValueToUint32(args[0])
+
+	s := getState(ctx)
+	oldmask := s.umask
+	s.umask = mask
+
+	return oldmask, nil
+}
+
 // jsfsMkdir implements implements jsFn for fs.Mkdir
 //
 //	jsFD /* Int */, err := fsCall("mkdir", path, perm)
@@ -452,7 +403,7 @@ type jsfsMkdir struct{}
 
 func (jsfsMkdir) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
 	path := resolvePath(ctx, args[0].(string))
-	perm := goos.ValueToUint32(args[1])
+	perm := getPerm(ctx, goos.ValueToUint32(args[1]))
 	callback := args[2].(funcWrapper)
 
 	fsc := mod.(*wasm.CallContext).Sys.FS()
@@ -464,7 +415,7 @@ func (jsfsMkdir) invoke(ctx context.Context, mod api.Module, args ...interface{}
 	if perm == 0 {
 		perm = 0o0500
 	}
-	if err = root.Mkdir(path, fs.FileMode(perm)); err == nil {
+	if err = root.Mkdir(path, perm); err == nil {
 		fd, err = fsc.OpenFile(root, path, os.O_RDONLY, 0)
 	}
 
@@ -544,11 +495,11 @@ type jsfsChmod struct{}
 
 func (jsfsChmod) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
 	path := resolvePath(ctx, args[0].(string))
-	mode := goos.ValueToUint32(args[1])
+	mode := custom.FromJsMode(goos.ValueToUint32(args[1]), 0)
 	callback := args[2].(funcWrapper)
 
 	fsc := mod.(*wasm.CallContext).Sys.FS()
-	err := fsc.RootFS().Chmod(path, fs.FileMode(mode))
+	err := fsc.RootFS().Chmod(path, mode)
 
 	return jsfsInvoke(ctx, mod, callback, err)
 }
@@ -560,7 +511,7 @@ type jsfsFchmod struct{}
 
 func (jsfsFchmod) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
 	fd := goos.ValueToUint32(args[0])
-	mode := goos.ValueToUint32(args[1])
+	mode := custom.FromJsMode(goos.ValueToUint32(args[1]), 0)
 	callback := args[2].(funcWrapper)
 
 	// Check to see if the file descriptor is available
@@ -571,7 +522,7 @@ func (jsfsFchmod) invoke(ctx context.Context, mod api.Module, args ...interface{
 	} else if chmodFile, ok := f.File.(chmodFile); !ok {
 		err = syscall.EBADF // possibly a fake file
 	} else {
-		err = chmodFile.Chmod(fs.FileMode(mode))
+		err = chmodFile.Chmod(mode)
 	}
 
 	return jsfsInvoke(ctx, mod, callback, err)
@@ -711,12 +662,12 @@ func (jsfsLink) invoke(ctx context.Context, mod api.Module, args ...interface{})
 type jsfsSymlink struct{}
 
 func (jsfsSymlink) invoke(ctx context.Context, mod api.Module, args ...interface{}) (interface{}, error) {
-	path := resolvePath(ctx, args[0].(string))
+	dst := args[0].(string) // The dst of a symlink must not be resolved, as it should be resolved during readLink.
 	link := resolvePath(ctx, args[1].(string))
 	callback := args[2].(funcWrapper)
 
 	fsc := mod.(*wasm.CallContext).Sys.FS()
-	err := fsc.RootFS().Symlink(path, link)
+	err := fsc.RootFS().Symlink(dst, link)
 
 	return jsfsInvoke(ctx, mod, callback, err)
 }
@@ -764,7 +715,7 @@ type jsSt struct {
 
 // String implements fmt.Stringer
 func (s *jsSt) String() string {
-	return fmt.Sprintf("{isDir=%v,mode=%s,size=%d,mtimeMs=%d}", s.isDir, fs.FileMode(s.mode), s.size, s.mtimeMs)
+	return fmt.Sprintf("{isDir=%v,mode=%s,size=%d,mtimeMs=%d}", s.isDir, custom.FromJsMode(s.mode, 0), s.size, s.mtimeMs)
 }
 
 // Get implements the same method as documented on goos.GetFunction
@@ -826,3 +777,10 @@ func resolvePath(ctx context.Context, path string) string {
 func joinPath(dirName, baseName string) string {
 	return path.Join(dirName, baseName)
 }
+
+// getPerm converts the input js permissions to a go-compatible one, after
+// subtracting the current umask.
+func getPerm(ctx context.Context, perm uint32) fs.FileMode {
+	umask := getState(ctx).umask
+	return custom.FromJsMode(perm, umask)
+}
diff --git a/internal/gojs/logging/logging.go b/internal/gojs/logging/logging.go
index 120093bc..9064ca26 100644
--- a/internal/gojs/logging/logging.go
+++ b/internal/gojs/logging/logging.go
@@ -298,11 +298,15 @@ func writeVal(w logging.Writer, name string, val interface{}) {
 		w.WriteString(name)                  //nolint
 		w.WriteString("_len=")               //nolint
 		writeI32(w, uint32(len(b.Unwrap()))) //nolint
-	} else if name == "perm" {
-		w.WriteString("perm=") //nolint
-		perm := goos.ValueToUint32(val)
-		w.WriteString(fs.FileMode(perm).String()) //nolint
-	} else {
+		return
+	}
+	switch name {
+	case "mask", "mode", "oldmask", "perm":
+		w.WriteString(name) //nolint
+		w.WriteByte('=')    //nolint
+		perm := custom.FromJsMode(goos.ValueToUint32(val), 0)
+		w.WriteString(perm.String()) //nolint
+	default:
 		w.WriteString(name)                   //nolint
 		w.WriteByte('=')                      //nolint
 		w.WriteString(fmt.Sprintf("%v", val)) //nolint
diff --git a/internal/gojs/state.go b/internal/gojs/state.go
index 17dc9fff..f089586f 100644
--- a/internal/gojs/state.go
+++ b/internal/gojs/state.go
@@ -24,6 +24,7 @@ func NewState(ctx context.Context) *State {
 		values:                 values.NewValues(),
 		valueGlobal:            newJsGlobal(getRoundTripper(ctx)),
 		cwd:                    getWorkdir(ctx),
+		umask:                  0o0022,
 		_nextCallbackTimeoutID: 1,
 		_scheduledTimeouts:     map[uint32]chan bool{},
 	}
@@ -190,6 +191,8 @@ type State struct {
 
 	// cwd is initially "/"
 	cwd string
+	// umask is initially 0022
+	umask uint32
 }
 
 // Get implements the same method as documented on goos.GetFunction
@@ -226,6 +229,7 @@ func (s *State) close() {
 	s._lastEvent = nil
 	s._nextCallbackTimeoutID = 1
 	s.cwd = "/"
+	s.umask = 0o0022
 }
 
 func toInt64(arg interface{}) int64 {


Test patch:
diff --git a/internal/gojs/custom/fs_test.go b/internal/gojs/custom/fs_test.go
new file mode 100644
index 00000000..d3da015f
--- /dev/null
+++ b/internal/gojs/custom/fs_test.go
@@ -0,0 +1,28 @@
+package custom
+
+import (
+	"io/fs"
+	"os"
+	"testing"
+
+	"github.com/tetratelabs/wazero/internal/testing/require"
+)
+
+func Test_ToJsMode(t *testing.T) {
+	t.Run("/dev/null", func(t *testing.T) {
+		st, err := os.Stat(os.DevNull)
+		require.NoError(t, err)
+
+		fm := ToJsMode(st.Mode())
+
+		// Should be a character device, and retain the permissions.
+		require.Equal(t, S_IFCHR|uint32(st.Mode().Perm()), fm)
+	})
+}
+
+func Test_FromJsMode(t *testing.T) {
+	t.Run("sticky bit", func(t *testing.T) {
+		jsMode := ToJsMode(0o0755 | fs.ModeSticky)
+		require.Equal(t, 0o0755|S_IFREG|S_ISVTX, jsMode)
+	})
+}
diff --git a/internal/gojs/fs_unit_test.go b/internal/gojs/fs_unit_test.go
deleted file mode 100644
index 3ad7eb7b..00000000
--- a/internal/gojs/fs_unit_test.go
+++ /dev/null
@@ -1,18 +0,0 @@
-package gojs
-
-import (
-	"os"
-	"testing"
-
-	"github.com/tetratelabs/wazero/internal/testing/require"
-)
-
-func Test_getWasiFiletype_DevNull(t *testing.T) {
-	st, err := os.Stat(os.DevNull)
-	require.NoError(t, err)
-
-	fm := getJsMode(st.Mode())
-
-	// Should be a character device, and retain the permissions.
-	require.Equal(t, S_IFCHR|uint32(st.Mode().Perm()), fm)
-}
diff --git a/internal/gojs/syscall_test.go b/internal/gojs/syscall_test.go
index 12c420c9..19d01738 100644
--- a/internal/gojs/syscall_test.go
+++ b/internal/gojs/syscall_test.go
@@ -19,7 +19,7 @@ syscall.Getppid()=0
 syscall.Getuid()=0
 syscall.Getgid()=0
 syscall.Geteuid()=0
-syscall.Umask(0077)=0o77
+syscall.Umask(0077)=0o22
 syscall.Getgroups()=[0]
 os.FindProcess(1).Pid=1
 `, stdout)
diff --git a/internal/gojs/testdata/writefs/main.go b/internal/gojs/testdata/writefs/main.go
index 4e9c399e..a04d2d02 100644
--- a/internal/gojs/testdata/writefs/main.go
+++ b/internal/gojs/testdata/writefs/main.go
@@ -282,4 +282,20 @@ func Main() {
 		return
 	}
 	defer os.Remove(dir)
+
+	// Symlink and Readlink tests.
+	s := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
+	from := "/symlink.txt"
+	err = os.Symlink(s, from)
+	if err != nil {
+		log.Panicln(err)
+	}
+
+	r, err := os.Readlink(from)
+	if err != nil {
+		log.Fatalf("readlink %q failed: %v", from, err)
+	}
+	if r != s {
+		log.Fatalf("after symlink %q != %q", r, s)
+	}
 }