|
| 1 | +(* |
| 2 | + * Copyright (C) 2023 Thomas Leonard |
| 3 | + * |
| 4 | + * Permission to use, copy, modify, and distribute this software for any |
| 5 | + * purpose with or without fee is hereby granted, provided that the above |
| 6 | + * copyright notice and this permission notice appear in all copies. |
| 7 | + * |
| 8 | + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES |
| 9 | + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF |
| 10 | + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR |
| 11 | + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES |
| 12 | + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN |
| 13 | + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF |
| 14 | + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
| 15 | + *) |
| 16 | + |
| 17 | +(* This module provides (optional) sandboxing, allowing operations to be restricted to a subtree. |
| 18 | +
|
| 19 | + For now, sandboxed directories use realpath and [O_NOFOLLOW], which is probably quite slow, |
| 20 | + and requires duplicating a load of path lookup logic from the kernel. |
| 21 | + It might be better to hold a directory FD rather than a path. |
| 22 | + On FreeBSD we could use O_RESOLVE_BENEATH and let the OS handle everything for us. |
| 23 | + On other systems we would have to resolve one path component at a time. *) |
| 24 | + |
| 25 | +open Eio.Std |
| 26 | + |
| 27 | +module Fd = Eio_unix.Fd |
| 28 | + |
| 29 | +class virtual posix_dir = object |
| 30 | + inherit Eio.Fs.dir |
| 31 | + |
| 32 | + val virtual opt_nofollow : Low_level.Flags.Open.t |
| 33 | + (** Extra flags for open operations. Sandboxes will add [O_NOFOLLOW] here. *) |
| 34 | + |
| 35 | + method virtual private resolve : string -> string |
| 36 | + (** [resolve path] returns the real path that should be used to access [path]. |
| 37 | + For sandboxes, this is [realpath path] (and it checks that it is within the sandbox). |
| 38 | + For unrestricted access, this is the identity function. *) |
| 39 | + |
| 40 | + method virtual with_parent_dir : 'a. (string -> (Fd.t option -> string -> 'a) -> 'a) |
| 41 | + (** [with_parent_dir path fn] runs [fn dir_fd rel_path], |
| 42 | + where [rel_path] accessed relative to [dir_fd] gives access to [path]. |
| 43 | + For unrestricted access, this just runs [fn None path]. |
| 44 | + For sandboxes, it opens the parent of [path] as [dir_fd] and runs [fn (Some dir_fd) (basename path)]. *) |
| 45 | +end |
| 46 | + |
| 47 | +(* When renaming, we get a plain [Eio.Fs.dir]. We need extra access to check |
| 48 | + that the new location is within its sandbox. *) |
| 49 | +type _ Eio.Generic.ty += Posix_dir : posix_dir Eio.Generic.ty |
| 50 | +let as_posix_dir x = Eio.Generic.probe x Posix_dir |
| 51 | + |
| 52 | +class virtual dir ~label = object (self) |
| 53 | + inherit posix_dir |
| 54 | + |
| 55 | + val mutable closed = false |
| 56 | + |
| 57 | + method! probe : type a. a Eio.Generic.ty -> a option = function |
| 58 | + | Posix_dir -> Some (self :> posix_dir) |
| 59 | + | _ -> None |
| 60 | + |
| 61 | + method open_in ~sw path = |
| 62 | + let open Low_level in |
| 63 | + let fd = Err.run (Low_level.openat ~sw (self#resolve path)) Low_level.Flags.Open.(rdonly) Flags.Disposition.(open_if) Flags.Create.(non_directory) in |
| 64 | + (Flow.of_fd fd :> <Eio.File.ro; Eio.Flow.close>) |
| 65 | + |
| 66 | + method open_out ~sw ~append ~create path = |
| 67 | + let open Low_level in |
| 68 | + let _mode, flags = |
| 69 | + match create with |
| 70 | + | `Never -> 0, Low_level.Flags.Open.empty |
| 71 | + | `If_missing perm -> perm, Low_level.Flags.Open.creat |
| 72 | + | `Or_truncate perm -> perm, Low_level.Flags.Open.(creat + trunc) |
| 73 | + | `Exclusive perm -> perm, Low_level.Flags.Open.(creat + excl) |
| 74 | + in |
| 75 | + let flags = if append then Low_level.Flags.Open.(flags + append) else flags in |
| 76 | + let flags = Low_level.Flags.Open.(flags + rdwr + opt_nofollow) in |
| 77 | + match |
| 78 | + self#with_parent_dir path @@ fun dirfd path -> |
| 79 | + Low_level.openat ?dirfd ~sw path flags Flags.Disposition.(open_if) Flags.Create.(non_directory) |
| 80 | + with |
| 81 | + | fd -> (Flow.of_fd fd :> <Eio.File.rw; Eio.Flow.close>) |
| 82 | + | exception Unix.Unix_error (ELOOP, _, _) -> |
| 83 | + (* The leaf was a symlink (or we're unconfined and the main path changed, but ignore that). |
| 84 | + A leaf symlink might be OK, but we need to check it's still in the sandbox. |
| 85 | + todo: possibly we should limit the number of redirections here, like the kernel does. *) |
| 86 | + let target = Unix.readlink path in |
| 87 | + let full_target = |
| 88 | + if Filename.is_relative target then |
| 89 | + Filename.concat (Filename.dirname path) target |
| 90 | + else target |
| 91 | + in |
| 92 | + self#open_out ~sw ~append ~create full_target |
| 93 | + | exception Unix.Unix_error (code, name, arg) -> |
| 94 | + raise (Err.wrap code name arg) |
| 95 | + |
| 96 | + method mkdir ~perm path = |
| 97 | + self#with_parent_dir path @@ fun dirfd path -> |
| 98 | + Err.run (Low_level.mkdir ?dirfd ~mode:perm) path |
| 99 | + |
| 100 | + method unlink path = |
| 101 | + self#with_parent_dir path @@ fun dirfd path -> |
| 102 | + Err.run (Low_level.unlink ?dirfd ~dir:false) path |
| 103 | + |
| 104 | + method rmdir path = |
| 105 | + self#with_parent_dir path @@ fun dirfd path -> |
| 106 | + Err.run (Low_level.unlink ?dirfd ~dir:true) path |
| 107 | + |
| 108 | + method read_dir path = |
| 109 | + (* todo: need fdopendir here to avoid races *) |
| 110 | + let path = self#resolve path in |
| 111 | + Err.run Low_level.readdir path |
| 112 | + |> Array.to_list |
| 113 | + |
| 114 | + method rename old_path new_dir new_path = |
| 115 | + match as_posix_dir new_dir with |
| 116 | + | None -> invalid_arg "Target is not an eio_posix directory!" |
| 117 | + | Some new_dir -> |
| 118 | + self#with_parent_dir old_path @@ fun old_dir old_path -> |
| 119 | + new_dir#with_parent_dir new_path @@ fun new_dir new_path -> |
| 120 | + Err.run (Low_level.rename ?old_dir old_path ?new_dir) new_path |
| 121 | + |
| 122 | + method open_dir ~sw path = |
| 123 | + Switch.check sw; |
| 124 | + let label = Filename.basename path in |
| 125 | + let d = new sandbox ~label (self#resolve path) in |
| 126 | + Switch.on_release sw (fun () -> d#close); |
| 127 | + (d :> Eio.Fs.dir_with_close) |
| 128 | + |
| 129 | + method close = closed <- true |
| 130 | + |
| 131 | + method pp f = Fmt.string f (String.escaped label) |
| 132 | +end |
| 133 | + |
| 134 | +and sandbox ~label dir_path = object (self) |
| 135 | + inherit dir ~label |
| 136 | + |
| 137 | + (* nofollow not on windows. *) |
| 138 | + val opt_nofollow = Low_level.Flags.Open.empty |
| 139 | + |
| 140 | + (* Resolve a relative path to an absolute one, with no symlinks. |
| 141 | + @raise Eio.Fs.Permission_denied if it's outside of [dir_path]. *) |
| 142 | + method private resolve path = |
| 143 | + if closed then Fmt.invalid_arg "Attempt to use closed directory %S" dir_path; |
| 144 | + if Filename.is_relative path then ( |
| 145 | + let dir_path = Err.run Low_level.realpath dir_path in |
| 146 | + let full = Err.run Low_level.realpath (Filename.concat dir_path path) in |
| 147 | + let prefix_len = String.length dir_path + 1 in |
| 148 | + (* \\??\\ Is necessary with NtCreateFile. *) |
| 149 | + if String.length full >= prefix_len && String.sub full 0 prefix_len = dir_path ^ Filename.dir_sep then |
| 150 | + "\\??\\" ^ full |
| 151 | + else if full = dir_path then |
| 152 | + "\\??\\" ^ full |
| 153 | + else |
| 154 | + raise @@ Eio.Fs.err (Permission_denied (Err.Outside_sandbox (full, dir_path))) |
| 155 | + ) else ( |
| 156 | + raise @@ Eio.Fs.err (Permission_denied Err.Absolute_path) |
| 157 | + ) |
| 158 | + |
| 159 | + method with_parent_dir path fn = |
| 160 | + if closed then Fmt.invalid_arg "Attempt to use closed directory %S" dir_path; |
| 161 | + let dir, leaf = Filename.dirname path, Filename.basename path in |
| 162 | + if leaf = ".." then ( |
| 163 | + (* We could be smarter here and normalise the path first, but '..' |
| 164 | + doesn't make sense for any of the current uses of [with_parent_dir] |
| 165 | + anyway. *) |
| 166 | + raise (Eio.Fs.err (Permission_denied (Err.Invalid_leaf leaf))) |
| 167 | + ) else ( |
| 168 | + let dir = self#resolve dir in |
| 169 | + Switch.run @@ fun sw -> |
| 170 | + let open Low_level in |
| 171 | + let dirfd = Low_level.openat ~sw dir Flags.Open.(rdonly) Flags.Disposition.(open_if) Flags.Create.(directory) in |
| 172 | + fn (Some dirfd) leaf |
| 173 | + ) |
| 174 | +end |
| 175 | + |
| 176 | +(* Full access to the filesystem. *) |
| 177 | +let fs = object |
| 178 | + inherit dir ~label:"fs" |
| 179 | + |
| 180 | + val opt_nofollow = Low_level.Flags.Open.empty |
| 181 | + |
| 182 | + (* No checks *) |
| 183 | + method private resolve path = path |
| 184 | + method private with_parent_dir path fn = fn None path |
| 185 | +end |
| 186 | + |
| 187 | +let cwd = new sandbox ~label:"cwd" "." |
0 commit comments