-
Notifications
You must be signed in to change notification settings - Fork 17.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proposal: os: safer file open functions #67002
Comments
is this essentially https://pkg.go.dev/github.com/google/safeopen with Beneath -> In ? that also has a ReadFile / WriteFile variant which I'd use more then the create version. |
The design of this proposal is influenced by github.com/google/safeopen, but differs in a few areas. (Sorry, I really should have mentioned safeopen as prior art.) Of the three parts of this proposal:
|
Yes, please. When I was working on safe file operations and it turned out to be hard to do correctly without OS support. Without |
What, if any, changes would be made to "io/fs"? Ideally, there is a mirror of these APIs in that package. |
If we wanted to extend this proposal to package fs
// An OpenFile is a directory file whose entries may be opened with the Open method.
type OpenFile interface {
File
// Open opens the named file in the directory.
//
// When Open returns an error, it should be of type *PathError
// with the Op field set to "openat", the Path field set to name,
// and the Err field describing the problem.
//
// Open should reject attempts to open names that do not
// satisfy ValidPath(name), returning a *PathError with Err set to
// ErrInvalid or ErrNotExist.
Open(name string) (File, error)
} A more interesting question is I don't think we can change The interaction between Perhaps we should add a version of |
Perhaps the new names should include an At suffix to make clear to casual readers of the API that these are not the usual open system calls. Either way, the three new methods should probably reference some shared section of documentation on the concept of the |
I presume you mean the new method names? The functions have an "In" suffix. We could also include the In suffix on the methods; I waffled on whether it belongs there or not:
I'm trying to avoid the suffix "At" to make it clear that none of these calls are precisely |
Fair enough. Should the |
Probably, for consistency. |
Are we missing RemoveIn? |
We should probably have // RemoveIn removes the named file or (empty) directory.
// It applies the same constraints on files as [OpenFileIn].
// It otherwise behaves like [Remove].
func RemoveIn(parent, name string) error
// Remove removes the named file or (empty) directory
// in the directory associated with the file f.
// It applies the same constraints on files as [File.OpenFile].
func (f *File) RemoveIn(name string) error Perhaps also |
This proposal has been added to the active column of the proposals project |
Maybe it would be better if the parent was an fs.FS? Seems more widely applicable, if somewhat more complex. |
Note that Windows does not provide (AFAIK) an func RemoveIn(parent, name string) error
f, err := os.OpenIn(parent, name)
if err != nil {
return err
}
return syscall.SetFileInformationByHandle(f.Fd(), syscall.FileDispositionInfo, ...)
} |
As a user, when should I use os.Open vs os.OpenIn? Should I continue to default to os.Open, and only use OpenIn when I am actively avoiding a security issue, or should my default be OpenIn now? |
The If we want to add support for
I think that's fine. This proposal requires varying degrees of implementation depending on platform already. (Linux has the very nice openat2 with RESOLVE_BENEATH, platforms without an equivalent are going to require us to do more work to produce equivalent behavior.) If it's not possible to emulate unlinkat on Windows, that might be a problem, but it sounds like it should be possible.
You should use I don't know how to give comprehensive guidance on when to use one vs. the other; the two functions behave differently and you should use the one that suits your specific purposes. If you're writing a command-line tool that accepts an input filename from the user, you probably want to use |
The FS OpenFile looks good, yes. Somehow I skipped that comment, sorry. |
I don't really understand this. What is a program supposed to do if
which is exactly the "hazardous" behaviour you say you're trying to avoid. If the underlying platform truly has no equivalent to I think this could be addressed perfectly well in the docs by saying that some platforms (linux, windows, etc) provide extra guarantees around renamed files, and that others (plan9 and js) do not. |
The question is whether this is a reasonable fallback or not. In the case of In the case of Perhaps it's okay to say that I note also that if you don't need the |
I would recommend adding guidance in the documentation of the not |
Updated proposal, with comments on various changes arising from above discussion and working on implementation. The package os
// OpenFileIn opens the named file in the named directory.
//
// If the file contains relative path components (..), no component may
// refer to a location outside the parent directory. The file may not be
// "", an absolute path, or (on Windows) a reserved device name such as "NUL".
// The file may refer to the directory itself (.).
//
// If any component of the named file references a symbolic link
// referencing a location out of the parent directory,
// OpenFileIn returns an error.
//
// OpenFileIn otherwise behaves like OpenFile.
func OpenFileIn(parent, name string, flag int, perm FileMode) (*File, error)
// CreateIn creates or truncates the named file in the named parent directory.
// It applies the same constraints on files as [OpenFileIn].
// It otherwise behaves like [Create].
func CreateIn(parent, name string) (*File, error)
// Open opens the named file in the named parent directory for reading.
// It applies the same constraints on files as [OpenFileIn].
// It otherwise behaves like [Open].
func OpenIn(parent, name string) (*File, error) The package os
// OpenFileIn opens the named file in the directory associated with the file f.
//
// If the file contains relative path components (..), no component may
// refer to a location outside the parent directory. The file may not be
// "", an absolute path, or (on Windows) a reserved device name such as "NUL".
//
// If any component of the named file references a symbolic link
// referencing a location out of the parent directory,
// OpenFileIn returns an error.
func (f *File) OpenFileIn(name string, flag int, perm FileMode) (*File, error)
// CreateIn creates or truncates the named file in
// the directory associated with the file f.
// It applies the same constraints on files as [File.OpenFile].
func (f *File) CreateIn(name string) (*File, error)
// OpenIn opens the named file in the directory associated with the file f for reading.
// It applies the same constraints on files as [File.OpenFile].
func (f *File) OpenIn(name string) (*File, error) To the above, we add Open question: Should we add package os
// MkdirIn creates a new directory in the named parent directory
// with the specified name and permission bits (before umask).
// It applies the same constraints on files as [OpenFileIn].
// It otherwise behaves like [Mkdir].
func MkdirIn(parent, name string, perm FileMode) error
// MkdirIn creates a new directory in the directory associated with the file f.
// It applies the same constraints on files as [File.OpenFile].
func (f *File) MkdirIn(name string, perm FileMode) error
// RemoveIn removes the named file or (empty) directory.
// It applies the same constraints on files as [OpenFileIn].
// It otherwise behaves like [Remove].
func RemoveIn(parent, name string) error
// RemoveIn removes the named file or (empty) directory
// in the directory associated with the file f.
// It applies the same constraints on files as [File.OpenFile].
func (f *File) RemoveIn(name string) error
// StatIn returns a FileInfo describing the named file in the named parent directory.
// It applies the same constraints on files as [OpenFileIn].
// It otherwise behaves like [Stat].
func StatIn(parent, name string) (FileInfo, error)
// StatIn returns a FileInfo describing the named file in the directory associated with the file f.
// It applies the same constraints on files as [File.OpenFile].
func (f *File) StatIn(name string) (FileInfo, error) We add Open question: For the moment, we do not add any new optional interfaces to There are many existing APIs, both in and out of the standard library, that operate on an Open question: It seems likely to me that we're going to want more variations on package os
// DirFSIn returns a filesystem for the tree of files rooted at the directory dir.
// The directory dir must not be "".
//
// Open calls will resolve symbolic links, but return an error if any link points outside the directory dir.
//
// The returned filesystem implements [io/fs.FS], [io/fs.StatFS], [io/fs.ReadFileFS], and [io/fs.ReadDirFS].
func DirFSIn(dir string) *FS
type FS struct{}
func (fs *FS) Open(name string) (File, error)
func (fs *FS) Stat(name string) (FileInfo, error)
func (fs *FS) ReadFile(name string) ([]byte, error)
func (fs *FS) ReadDir(name string) ([]fs.DirEntry, error) The Open question: Should we add const (
// O_NOFOLLOW_ANY, when included in the flags passed to [OpenFile], [OpenFileIn],
// or [File.OpenFile], disallows resolution of symbolic links anywhere in the
// named file.
//
// O_NOFOLLOW_ANY affects the handling of symbolic links in all components
// of the filename. (In contrast, the O_NOFOLLOW flag supported by many
// platforms only affects resolution of the last path component.)
//
// O_NOFOLLOW_ANY does not disallow symbolic links in the parent directory name
// parameter of [OpenFileIn].
//
// O_NOFOLLOW_ANY does not affect traversal of hard links, Windows junctions,
// or Plan 9 bind mounts.
//
// On platforms which support symbolic links but do not provide a way to
// disable symbolic link traversal (GOOS=js), open functions return an error
// if O_NOFOLLOW_ANY is provided.
O_NOFOLLOW_ANY int = (some value)
) Open question: How should we handle Consider the following directory tree:
On the Unix command line, if we If we open the current directory and The On Windows, things are confusing (and I'm still trying to understand what's going on under the hood): Using
It appears that The question is: What should
My current inclination is the first option above: Permit both symlinks and I can, however, see a good argument for disallowing Open question: How should we handle platforms without GOOS=js does not permit implementing GOOS=js and GOOS=plan9 do not permit implementing I've argued above for supporting |
This is rather extensive API. Perhaps a separate package from os would be better? Maybe os/in? |
On a very minor note, Plan 9 can be considered to implement O_NOFOLLOW_ANY because there are no symlinks on Plan 9 at all. |
More generally, I understand the motivation here, but the amount of new API is a bit daunting. I think we need to keep thinking about reducing the total amount of API. It seems like there needs to be some type representing the constrained file system. For this message, let's call it a Dir. It would be defined like:
All the top-level convenience things like os.OpenIn can be left out. Code can use OpenDir followed by the operation it wants. That at least feels like a more manageable amount of API. I have been thinking for a while and have not come up with a name like more than Dir. It's certainly not perfect, and OpenDir would need a doc comment explaining that it's not opendir(3), but it's not bad. |
@rsc With this approach it would be also nice to define some globals, like |
Also // Methods on Dir are safe to be used from multiple goroutines simultaneously. Is this true for |
@mateusz834 Sure, Close can be called from multiple goroutines simultaneously. Same thing is true of os.File. |
A few comments:
|
I think this is an oversight. The list includes every top-level os function which operates on files. We have os.Link, so we should have Root.Link as well:
I agree that Root.Truncate doesn't seem very necessary when File.Truncate exists, but supporting every file operation on a Root is simpler than picking and choosing the "necessary" ones.
Root.OpenFile can be used with O_NOFOLLOW, just like the top-level OpenFIle:
If we want to offer alternate ways of opening files in the future, we can add methods to Root to change its behavior. For example:
I don't understand the attack vector here. Can you explain in more detail? A Root prohibits escapes via path traversal and symlinks. If you Root.OpenRoot a subdirectory, the new Root prohibits escapes from that subdirectory. Moving the directory doesn't change that. Root.OpenRoot is essentially open with O_PATH, and is useful for efficiently implementing functions which traverse a directory tree such as os.RemoveAll.
I'm afraid I don't understand the concern here. There is no syscall (so far as I know) that implements MkdirAll, so whatever behavior we have here will be in user space. Is a "naive userspace resolver" one which doesn't implement symlink traversal correctly? If so, I do not believe the path resolution in https://go.dev/cl/612136 is naive.
Root defends against three classes of attacks:
Without openat or some equivalent, we can still defend against path name traversal and symlink traversal, but we are vulnerable to TOCTOU races in symlink traversal. There is benefit in defending against traversal. Of the real vulnerabilities that I've seen that Root might defend against, I think a strict majority have been in cases where TOCTOU was not a concern. For example, CVE-2024-3000 was a real and significant vulnerability that didn't involve symlinks at all. In my initial version of this proposal, I proposed that the new API return an error on GOOS=js. Subsequent discussion convinced me that this was a mistake, and that we should implement as much of the new API as is feasible on each platform. Not supporting os.Root on some platforms will give users a reason not to use it at all.
Yes, os.RemoveAll and os.Root.RemoveAll will share a common implementation, and we will fix #52745 (except for GOOS=js) as part of this proposal. |
Change https://go.dev/cl/617378 mentions this issue: |
Change https://go.dev/cl/617376 mentions this issue: |
Change https://go.dev/cl/617377 mentions this issue: |
Mostly copied from x/sys/windows. This adds a various related types and functions, but the purpose is to give access to NtCreateFile, which can be used as an equivalent to openat. For #67002 Change-Id: I04e6f630445a55c2000c9c323ce8dcdc7fc0d0e0 Reviewed-on: https://go-review.googlesource.com/c/go/+/617377 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Quim Muntal <quimmuntal@gmail.com> Reviewed-by: Ian Lance Taylor <iant@google.com>
For #67002 Change-Id: I460e02db33799c145c296bcf0668fa555199036e Reviewed-on: https://go-review.googlesource.com/c/go/+/617376 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Ian Lance Taylor <iant@golang.org> Reviewed-by: Cherry Mui <cherryyz@google.com>
The syscall package is mostly frozen, but wasip1 file syscall support was added to syscall and the Open and Openat implementations overlap. Implement Openat in syscall for overall simplicity. We already have syscall.Openat for some platforms, so this doesn't add any new functions to syscall. For #67002 Change-Id: Ia34b12ef11fc7a3b7832e07b3546a760c23efe5b Reviewed-on: https://go-review.googlesource.com/c/go/+/617378 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Ian Lance Taylor <iant@golang.org> Reviewed-by: Cherry Mui <cherryyz@google.com>
Explaining this before you see the issue in practice is a little complicated, but I'll try my best: The way I implemented The problem is that if the userspace symlink resolver just prepends symlinks to the "remaining path" (which is what your current resolver does), if you hit a dangling symlink the userspace implementation will have behaviour that won't match an
The final The solution I ended up with was creating a "symlink stack" that tracks whether or not the userspace resolver is still in the middle of symlink resolution. The PRs and code I linked to show how I did it in case having a reference might help. https://github.com/cyphar/filepath-securejoin/blob/main/lookup_linux.go is the "non naive" resolver I ended up with as a result. I only mentioned it because it will make your resolver a fair bit more complicated, and I was wondering whether it makes to have this somewhat ugly thing in the stdlib in the first pass of this API. But then again,
The attacks are similar to The rename attack I was talking about is that the attacker can move the root of the resolution in a way that
Now, your retry-based implementation is not immediately vulnerable to this particular attack, but that doesn't mean there isn't some other similar attack that we just can't think of at the moment. To paraphrase Jann Horn's comments on
I guess this depends on the projects you work on. My experience is that while non-TOCTOU cases do happen a fair bit1, a lot of programs are run by users in contexts where the TOCTOU case becomes important at some point in the future. You're quite right that securing the more egregious cases is worth doing, but I just can't shake the concern that having slightly different security semantics on different platforms is something that is going to bite people, even if it is documented. 1: One of my first patches against Docker a decade ago was fixing a non-TOCTOU |
It sounds like you're describing an implementation of Root.MkdirAll that's something like:
That seems obviously incorrect: Mkdir on a dangling symlink is supposed to fail, but this follows symlinks. It also doesn't seem any simpler than a correct implementation. A simple and (I believe) correct implementation would be to take the current os.MkdirAll and convert it to call Root.Mkdir rather than os.Mkdir. This implementation is:
Within a Root, this can be fairly inefficient, because each Mkdir may traverse the entire directory tree. A more efficient approach can walk the hierarchy only once:
(We then need a bit of complexity to handle I sketched an implementation of this approach in https://go.dev/cl/619076. |
It is, of course, possible--probable, even--that we're going to discover attacks that we haven't accounted for. I don't see, however, how |
The second step is done with a very restricted
That does work but now you're looking at O(n^3) complexity for a path that is potentially attacker-controlled and is not restricted by Also (and to be fair -- this is a little esoteric and might not be a real problem, but) an operation like
Unless I'm missing something this also won't work for symlinks that contain (FWIW this approach also just doesn't work at all for |
The difference is that This means an attacker can do rename operations (among other things) on the root itself for
How about making it private until someone shows up that has a strong usecase for why it should be part of the API in a way that encourages users to do something that is potentially less safe? |
Change https://go.dev/cl/619435 mentions this issue: |
Good point (that'll teach me to put together an example CL without writing tests). The real implementation will need a bit more complexity to handle that case. (I suspect the real implementation will involve generalizing
I still don't see how Root.OpenRoot is less safe. A Root, on platforms with openat, is essentially a file descriptor. Root.Open and Root.OpenRoot both open a file descriptor within a Root; the only difference is the type returned (*File or *Root). I don't see a scenario where Root.Open and Root.OpenRoot don't share the same set of vulnerabilities: If one can escape the root in some scenario, so can the other. So adding OpenRoot doesn't increase the attack surface. And once a Root is created, it doesn't matter whether it was created by os.OpenRoot or Root.OpenRoot; in both cases, it's just a file descriptor. |
Windows versions of openat and mkdirat, implemented using NtCreateFile. For #67002 Change-Id: If43b1c1069733e5c45f7d45a69699fec30187308 Reviewed-on: https://go-review.googlesource.com/c/go/+/619435 Reviewed-by: Cherry Mui <cherryyz@google.com> Reviewed-by: Quim Muntal <quimmuntal@gmail.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Change https://go.dev/cl/620157 mentions this issue: |
Refactor TestOpenError to use relative paths in test cases, in preparation for extending it to test os.Root. Use a test temporary directory instead of system directory with presumed-known contents. Move the testcase type and case definitions inline with the test. For #67002 Change-Id: Idc53dd9fcecf763d3e4eb3b4643032e3003d7ef4 Reviewed-on: https://go-review.googlesource.com/c/go/+/620157 Reviewed-by: Ian Lance Taylor <iant@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
The difference is that by design |
Late to the discussion, but I thought I'd point out that the hcsshim project has an implementation of a similar thing for Windows, used when extracting container images to ensure reparse points cannot be used to break out of the container image's data directory. Because in that case following a reparse point is always invalid, their version is simpler and rejects any reparse points it encounters, so it does not need to resolve or validate the resulting path for traversal issues. It's public operations are: One specific thing worth calling out is that Apart from that and That said, I'm not sure if the hcsshim code could be reimplemented on top of this new feature because of the requirement to reject any reparse points encountered, rather than resolve them. |
Change https://go.dev/cl/620576 mentions this issue: |
Whenn O_TRUNC is set, Opentat ends up calling syscall.Ftruncate, which needs write access. Make sure write access is not removed when O_TRUNC and O_APPEND are both set. Updates #67002. Change-Id: Iccc470b7be3c62144318d6a707057504f3b74c97 Reviewed-on: https://go-review.googlesource.com/c/go/+/620576 Reviewed-by: Alex Brainman <alex.brainman@gmail.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Auto-Submit: Damien Neil <dneil@google.com> Reviewed-by: Damien Neil <dneil@google.com> Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
For consistency with #49580, should the For the record, There seem to be two questions here: Should
I think we could add an |
I think we should have If we want As you say, It might make sense to have an |
Should You can currently list the contents of a If we do have a
|
We can add There's been a fair amount of discussion since I checked for comments. Any further comments? |
Presumably you can just use |
Have all remaining concerns about this proposal been addressed? The proposal is: package os
// Root represents a directory.
//
// Methods on Root can only access files and directories within that directory.
// If any component of a file name passed to a method of Root references a location
// outside the root, the method returns an error.
// File names may reference the directory itself (.).
//
// File names may contain symbolic links, but symbolic links may not
// reference a location outside the root.
// Symbolic links must not be absolute.
//
// Methods on Root do not prohibit traversal of filesystem boundaries,
// Linux bind mounts, /proc special files, or access to Unix device files.
//
// Methods on Root are safe to be used from multiple goroutines simultaneously.
//
// On most platforms, creating a Root opens a file descriptor or handle referencing
// the directory. If the directory is moved, methods on Root reference the original
// directory.
//
// Root's behavior differs on some platforms:
//
// - When GOOS=windows, file names may not reference Windows reserved device names
// such as NUL and COM1.
// - When GOOS=js, Root is vulnerable to TOCTOU (time-of-check-time-of-use)
// attacks in symlink validation, and cannot ensure that operations will not
// escape the root.
// - When GOOS=plan9 or GOOS=js, Root does not track directories across renames.
// On these platforms, a Root references a directory name, not a file descriptor
type Root struct { ... }
func OpenRoot(dir string) (*Root, error)
func (*Root) FS() fs.FS
func (*Root) OpenFile
func (*Root) Create
func (*Root) Open
func (*Root) OpenRoot
func (*Root) Close
func (*Root) Mkdir
func (*Root) Remove
func (*Root) MkdirAll
func (*Root) RemoveAll
func (*Root) Chmod
func (*Root) Chown
func (*Root) Chtimes
func (*Root) Lchown
func (*Root) Lstat
func (*Root) Readlink
func (*Root) Rename
func (*Root) Stat
func (*Root) Symlink
func (*Root) Link
func (*Root) Truncate
func OpenInRoot(dir, name string) (*File, error) {
r, err := OpenRoot(dir)
if err != nil { return nil }
return r.Open(name)
} |
Please see the updated proposal in #67002 (comment)
Directory traversal vulnerabilities are a common class of vulnerability, in which an attacker tricks a program into opening a file that it did not intend. These attacks often take the form of providing a relative pathname such as
"../../../etc/passwd"
, which results in access outside an intended location. CVE-2024-3400 is a recent, real-world example of directory traversal leading to an actively exploited remote code execution vulnerability.A related, but less commonly exploited, class of vulnerability involves unintended symlink traversal, in which an attacker creates a symbolic link in the filesystem and manipulates the target into following it.
I propose adding several new functions to the os package to aid in safely opening files with untrusted filename components and defending against symlink traversal.
It is very common for programs to open a file in a known location using an untrusted filename. Programs can avoid directory traversal attacks by first validating the filename with a function like
filepath.IsLocal
. Defending against symlink traversal is harder.I propose adding functions to open a file in a location:
The
OpenFileIn
,OpenIn
, andCreateIn
family of functions safely open a file within a given location, defending against directory traversal, symlinks to unexpected locations, and unexpected access to Windows device files.All modern Unix systems that I know of provide an
openat
call, to open a file relative to an existing directory handle (FD). Windows provides an equivalent (NtCreateFile
withObjectAttributes
including aRootDirectory
). Of the supported Go ports, I believe only js and plan9 do not supportopenat
or an equivalent.I propose adding support for
openat
-like behavior toos.File
:Like the top-level
CreateIn
,OpenIn
, andOpenFileIn
, the methods defend against accessing files outside the given directory. This is unlike the default behavior ofopenat
, which permits absolute paths, relative paths outside the root, and symlink traversal outside the root. (It corresponds to Linux'sopenat2
with theRESOLVE_BENEATH
flag.)A property of
openat
is that it follows a file across renames: If you open a directory, rename the directory, and useopenat
on the still-open FD, access is relative to the directory's new location. We cannot support this behavior on platforms which don't haveopenat
or an equivalent (plan9 and js). We could fall back to operating purely on filenames, such thatf.OpenIn(x)
is equivalent toos.OpenIn(f.Name(), x)
. However, this seems potentially hazardous. I propose, therefore, thatFile.CreateIn
,File.OpenIn
, andFile.OpenFileIn
return anerrors.ErrUnsupported
error on these platforms.The above functions defend against symlink traversal that leads outside of the designated root directory. Some users may wish to defend against symlink traversal entirely. Many modern operating systems provide an easy way to disable symlink following: Linux has
RESOLVE_NO_SYMLINKS
, Darwin hasO_NOFOLLOW_ANY
, and some other platforms have equivalents.I propose adding support for disabling symlink traversal to the
os
package:O_NOFOLLOW_ANY
may be passed toOpenFile
,OpenFIleIn
, orFile.OpenFIle
to disable symlink traversal in any component of the file name. ForOpenFileIn
, symlinks would still be permitted in the directory component.On platforms which do not support the equivalent of
O_NOFOLLOW_ANY
/RESOLVE_NO_SYMLINKS
natively, theos
package will use successiveopenat
calls withO_NOFOLLOW
to emulate it. On platforms with noopenat
(plan9 and js), open operations will return an error whenO_NOFOLLOW_ANY
is specified.The text was updated successfully, but these errors were encountered: