1// Copyright 2023 The Go Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5package poll 6 7import ( 8 "internal/byteorder" 9 "sync/atomic" 10 "syscall" 11 "unsafe" 12) 13 14type SysFile struct { 15 // RefCountPtr is a pointer to the reference count of Sysfd. 16 // 17 // WASI preview 1 lacks a dup(2) system call. When the os and net packages 18 // need to share a file/socket, instead of duplicating the underlying file 19 // descriptor, we instead provide a way to copy FD instances and manage the 20 // underlying file descriptor with reference counting. 21 RefCountPtr *int32 22 23 // RefCount is the reference count of Sysfd. When a copy of an FD is made, 24 // it points to the reference count of the original FD instance. 25 RefCount int32 26 27 // Cache for the file type, lazily initialized when Seek is called. 28 Filetype uint32 29 30 // If the file represents a directory, this field contains the current 31 // readdir position. It is reset to zero if the program calls Seek(0, 0). 32 Dircookie uint64 33 34 // Absolute path of the file, as returned by syscall.PathOpen; 35 // this is used by Fchdir to emulate setting the current directory 36 // to an open file descriptor. 37 Path string 38 39 // TODO(achille): it could be meaningful to move isFile from FD to a method 40 // on this struct type, and expose it as `IsFile() bool` which derives the 41 // result from the Filetype field. We would need to ensure that Filetype is 42 // always set instead of being lazily initialized. 43} 44 45func (s *SysFile) init() { 46 if s.RefCountPtr == nil { 47 s.RefCount = 1 48 s.RefCountPtr = &s.RefCount 49 } 50} 51 52func (s *SysFile) ref() SysFile { 53 atomic.AddInt32(s.RefCountPtr, +1) 54 return SysFile{RefCountPtr: s.RefCountPtr} 55} 56 57func (s *SysFile) destroy(fd int) error { 58 if s.RefCountPtr != nil && atomic.AddInt32(s.RefCountPtr, -1) > 0 { 59 return nil 60 } 61 62 // We don't use ignoringEINTR here because POSIX does not define 63 // whether the descriptor is closed if close returns EINTR. 64 // If the descriptor is indeed closed, using a loop would race 65 // with some other goroutine opening a new descriptor. 66 // (The Linux kernel guarantees that it is closed on an EINTR error.) 67 return CloseFunc(fd) 68} 69 70// Copy creates a copy of the FD. 71// 72// The FD instance points to the same underlying file descriptor. The file 73// descriptor isn't closed until all FD instances that refer to it have been 74// closed/destroyed. 75func (fd *FD) Copy() FD { 76 return FD{ 77 Sysfd: fd.Sysfd, 78 SysFile: fd.SysFile.ref(), 79 IsStream: fd.IsStream, 80 ZeroReadIsEOF: fd.ZeroReadIsEOF, 81 isBlocking: fd.isBlocking, 82 isFile: fd.isFile, 83 } 84} 85 86// dupCloseOnExecOld always errors on wasip1 because there is no mechanism to 87// duplicate file descriptors. 88func dupCloseOnExecOld(fd int) (int, string, error) { 89 return -1, "dup", syscall.ENOSYS 90} 91 92// Fchdir wraps syscall.Fchdir. 93func (fd *FD) Fchdir() error { 94 if err := fd.incref(); err != nil { 95 return err 96 } 97 defer fd.decref() 98 return syscall.Chdir(fd.Path) 99} 100 101// ReadDir wraps syscall.ReadDir. 102// We treat this like an ordinary system call rather than a call 103// that tries to fill the buffer. 104func (fd *FD) ReadDir(buf []byte, cookie syscall.Dircookie) (int, error) { 105 if err := fd.incref(); err != nil { 106 return 0, err 107 } 108 defer fd.decref() 109 for { 110 n, err := syscall.ReadDir(fd.Sysfd, buf, cookie) 111 if err != nil { 112 n = 0 113 if err == syscall.EAGAIN && fd.pd.pollable() { 114 if err = fd.pd.waitRead(fd.isFile); err == nil { 115 continue 116 } 117 } 118 } 119 // Do not call eofError; caller does not expect to see io.EOF. 120 return n, err 121 } 122} 123 124func (fd *FD) ReadDirent(buf []byte) (int, error) { 125 n, err := fd.ReadDir(buf, fd.Dircookie) 126 if err != nil { 127 return 0, err 128 } 129 if n <= 0 { 130 return n, nil // EOF 131 } 132 133 // We assume that the caller of ReadDirent will consume the entire buffer 134 // up to the last full entry, so we scan through the buffer looking for the 135 // value of the last next cookie. 136 b := buf[:n] 137 138 for len(b) > 0 { 139 next, ok := direntNext(b) 140 if !ok { 141 break 142 } 143 size, ok := direntReclen(b) 144 if !ok { 145 break 146 } 147 if size > uint64(len(b)) { 148 break 149 } 150 fd.Dircookie = syscall.Dircookie(next) 151 b = b[size:] 152 } 153 154 // Trim a potentially incomplete trailing entry; this is necessary because 155 // the code in src/os/dir_unix.go does not deal well with partial values in 156 // calls to direntReclen, etc... and ends up causing an early EOF before all 157 // directory entries were consumed. ReadDirent is called with a large enough 158 // buffer (8 KiB) that at least one entry should always fit, tho this seems 159 // a bit brittle but cannot be addressed without a large change of the 160 // algorithm in the os.(*File).readdir method. 161 return n - len(b), nil 162} 163 164// Seek wraps syscall.Seek. 165func (fd *FD) Seek(offset int64, whence int) (int64, error) { 166 if err := fd.incref(); err != nil { 167 return 0, err 168 } 169 defer fd.decref() 170 // syscall.Filetype is a uint8 but we store it as a uint32 in SysFile in 171 // order to use atomic load/store on the field, which is why we have to 172 // perform this type conversion. 173 fileType := syscall.Filetype(atomic.LoadUint32(&fd.Filetype)) 174 175 if fileType == syscall.FILETYPE_UNKNOWN { 176 var stat syscall.Stat_t 177 if err := fd.Fstat(&stat); err != nil { 178 return 0, err 179 } 180 fileType = stat.Filetype 181 atomic.StoreUint32(&fd.Filetype, uint32(fileType)) 182 } 183 184 if fileType == syscall.FILETYPE_DIRECTORY { 185 // If the file descriptor is opened on a directory, we reset the readdir 186 // cookie when seeking back to the beginning to allow reusing the file 187 // descriptor to scan the directory again. 188 if offset == 0 && whence == 0 { 189 fd.Dircookie = 0 190 return 0, nil 191 } else { 192 return 0, syscall.EINVAL 193 } 194 } 195 196 return syscall.Seek(fd.Sysfd, offset, whence) 197} 198 199// https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md#-dirent-record 200const sizeOfDirent = 24 201 202func direntReclen(buf []byte) (uint64, bool) { 203 namelen, ok := direntNamlen(buf) 204 return sizeOfDirent + namelen, ok 205} 206 207func direntNamlen(buf []byte) (uint64, bool) { 208 return readInt(buf, unsafe.Offsetof(syscall.Dirent{}.Namlen), unsafe.Sizeof(syscall.Dirent{}.Namlen)) 209} 210 211func direntNext(buf []byte) (uint64, bool) { 212 return readInt(buf, unsafe.Offsetof(syscall.Dirent{}.Next), unsafe.Sizeof(syscall.Dirent{}.Next)) 213} 214 215// readInt returns the size-bytes unsigned integer in native byte order at offset off. 216func readInt(b []byte, off, size uintptr) (u uint64, ok bool) { 217 if len(b) < int(off+size) { 218 return 0, false 219 } 220 return readIntLE(b[off:], size), true 221} 222 223func readIntLE(b []byte, size uintptr) uint64 { 224 switch size { 225 case 1: 226 return uint64(b[0]) 227 case 2: 228 return uint64(byteorder.LeUint16(b)) 229 case 4: 230 return uint64(byteorder.LeUint32(b)) 231 case 8: 232 return uint64(byteorder.LeUint64(b)) 233 default: 234 panic("internal/poll: readInt with unsupported size") 235 } 236} 237