Skip to content

Commit c9f67ae

Browse files
committed
eio_posix: use directory FDs instead of realpath
realpath was an old hack from the libuv days.
1 parent f9ba4ca commit c9f67ae

File tree

19 files changed

+598
-181
lines changed

19 files changed

+598
-181
lines changed

lib_eio/unix/eio_unix.mli

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ module Private : sig
103103
module Thread_pool = Thread_pool
104104

105105
val read_link : Fd.t option -> string -> string
106+
val read_link_unix : Unix.file_descr option -> string -> string
106107
end
107108

108109
module Pi = Pi

lib_eio/unix/fd.ml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ let rec use_exn_list op xs k =
8787
use_exn_list op xs @@ fun xs ->
8888
k (x :: xs)
8989

90+
let use_exn_opt op x f =
91+
match x with
92+
| None -> f None
93+
| Some x -> use_exn op x (fun x -> f (Some x))
94+
9095
let stdin = of_unix_no_hook Unix.stdin
9196
let stdout = of_unix_no_hook Unix.stdout
9297
let stderr= of_unix_no_hook Unix.stderr

lib_eio/unix/fd.mli

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ val use_exn : string -> t -> (Unix.file_descr -> 'a) -> 'a
3535
val use_exn_list : string -> t list -> (Unix.file_descr list -> 'a) -> 'a
3636
(** [use_exn_list op fds fn] calls {!use_exn} on each FD in [fds], calling [fn wrapped_fds] on the results. *)
3737

38+
val use_exn_opt : string -> t option -> (Unix.file_descr option -> 'a) -> 'a
39+
(** [use_exn_opt op fd fn] is like {!use_exn}, but if [fd = None] then it just calls [fn None]. *)
40+
3841
(** {2 Closing} *)
3942

4043
val close : t -> unit

lib_eio/unix/private.ml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,16 @@ module Thread_pool = Thread_pool
2020

2121
external eio_readlinkat : Unix.file_descr -> string -> Cstruct.t -> int = "eio_unix_readlinkat"
2222

23-
let read_link fd path =
23+
let read_link_unix fd path =
2424
match fd with
2525
| None -> Unix.readlink path
2626
| Some fd ->
27-
Fd.use_exn "readlink" fd @@ fun fd ->
2827
let rec aux size =
2928
let buf = Cstruct.create_unsafe size in
3029
let len = eio_readlinkat fd path buf in
3130
if len < size then Cstruct.to_string ~len buf
3231
else aux (size * 4)
3332
in
3433
aux 1024
34+
35+
let read_link fd path = Fd.use_exn_opt "readlink" fd (fun fd -> read_link_unix fd path)

lib_eio_posix/eio_posix_stubs.c

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include <fcntl.h>
1818
#include <string.h>
1919
#include <stdlib.h>
20+
#include <dirent.h>
2021

2122
#include <caml/mlvalues.h>
2223
#include <caml/memory.h>
@@ -527,3 +528,13 @@ CAMLprim value caml_eio_posix_recv_msg(value v_fd, value v_max_fds, value v_bufs
527528

528529
CAMLreturn(v_result);
529530
}
531+
532+
CAMLprim value caml_eio_posix_fdopendir(value v_fd) {
533+
DIR *d = fdopendir(Int_val(v_fd));
534+
if (!d)
535+
caml_uerror("fdopendir", Nothing);
536+
537+
value v_result = caml_alloc_small(1, Abstract_tag);
538+
DIR_Val(v_result) = d;
539+
return v_result;
540+
}

lib_eio_posix/err.ml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
type Eio.Exn.Backend.t +=
2-
| Outside_sandbox of string * string
2+
| Outside_sandbox of string
33
| Absolute_path
44
| Invalid_leaf of string
55

66
let unclassified_error e = Eio.Exn.create (Eio.Exn.X e)
77

88
let () =
99
Eio.Exn.Backend.register_pp (fun f -> function
10-
| Outside_sandbox (path, dir) -> Fmt.pf f "Outside_sandbox (%S, %S)" path dir; true
10+
| Outside_sandbox path -> Fmt.pf f "Outside_sandbox (%S)" path; true
1111
| Absolute_path -> Fmt.pf f "Absolute_path"; true
1212
| Invalid_leaf x -> Fmt.pf f "Invalid_leaf %S" x; true
1313
| _ -> false

lib_eio_posix/fs.ml

Lines changed: 35 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -16,84 +16,44 @@
1616

1717
(* This module provides (optional) sandboxing, allowing operations to be restricted to a subtree.
1818
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. *)
19+
On FreeBSD we use O_RESOLVE_BENEATH and let the OS handle everything for us.
20+
On other systems we resolve one path component at a time. *)
2421

2522
open Eio.Std
2623

2724
module Fd = Eio_unix.Fd
2825

26+
(* When renaming, we get a plain [Eio.Fs.dir]. We need extra access to check
27+
that the new location is within its sandbox. *)
28+
type (_, _, _) Eio.Resource.pi += Posix_dir : ('t, 't -> Low_level.dir_fd, [> `Posix_dir]) Eio.Resource.pi
29+
30+
let as_posix_dir (Eio.Resource.T (t, ops)) =
31+
match Eio.Resource.get_opt ops Posix_dir with
32+
| None -> None
33+
| Some fn -> Some (fn t)
34+
2935
module rec Dir : sig
3036
include Eio.Fs.Pi.DIR
3137

32-
val v : label:string -> sandbox:bool -> string -> t
33-
34-
val resolve : t -> string -> string
35-
(** [resolve t path] returns the real path that should be used to access [path].
36-
For sandboxes, this is [realpath path] (and it checks that it is within the sandbox).
37-
For unrestricted access, this returns [path] unchanged.
38-
@raise Eio.Fs.Permission_denied if sandboxed and [path] is outside of [dir_path]. *)
38+
val v : label:string -> path:string -> Low_level.dir_fd -> t
3939

40-
val with_parent_dir : t -> string -> (Fd.t option -> string -> 'a) -> 'a
41-
(** [with_parent_dir t 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)]. *)
40+
val fd : t -> Low_level.dir_fd
4541
end = struct
4642
type t = {
43+
fd : Low_level.dir_fd;
4744
dir_path : string;
48-
sandbox : bool;
4945
label : string;
50-
mutable closed : bool;
5146
}
5247

53-
let resolve t path =
54-
if t.sandbox then (
55-
if t.closed then Fmt.invalid_arg "Attempt to use closed directory %S" t.dir_path;
56-
if Filename.is_relative path then (
57-
let dir_path = Err.run Low_level.realpath t.dir_path in
58-
let full = Err.run Low_level.realpath (Filename.concat dir_path path) in
59-
let prefix_len = String.length dir_path + 1 in
60-
if String.length full >= prefix_len && String.sub full 0 prefix_len = dir_path ^ Filename.dir_sep then
61-
full
62-
else if full = dir_path then
63-
full
64-
else
65-
raise @@ Eio.Fs.err (Permission_denied (Err.Outside_sandbox (full, dir_path)))
66-
) else (
67-
raise @@ Eio.Fs.err (Permission_denied Err.Absolute_path)
68-
)
69-
) else path
70-
71-
let with_parent_dir t path fn =
72-
if t.sandbox then (
73-
if t.closed then Fmt.invalid_arg "Attempt to use closed directory %S" t.dir_path;
74-
let dir, leaf = Filename.dirname path, Filename.basename path in
75-
let dir, leaf =
76-
if leaf = ".." then path, "."
77-
else dir, leaf
78-
in
79-
let dir = resolve t dir in
80-
Switch.run ~name:"with_parent_dir" @@ fun sw ->
81-
let dirfd = Low_level.openat ~sw ~mode:0 dir Low_level.Open_flags.(directory + rdonly + nofollow) in
82-
fn (Some dirfd) leaf
83-
) else fn None path
84-
85-
let v ~label ~sandbox dir_path = { dir_path; sandbox; label; closed = false }
48+
let fd t = t.fd
8649

87-
(* Sandboxes use [O_NOFOLLOW] when opening files ([resolve] already removed any symlinks).
88-
This avoids a race where symlink might be added after [realpath] returns. *)
89-
let opt_nofollow t =
90-
if t.sandbox then Low_level.Open_flags.nofollow else Low_level.Open_flags.empty
50+
let v ~label ~path:dir_path fd = { fd; dir_path; label }
9151

9252
let open_in t ~sw path =
93-
let fd = Err.run (Low_level.openat ~mode:0 ~sw (resolve t path)) Low_level.Open_flags.(opt_nofollow t + rdonly) in
53+
let fd = Err.run (Low_level.openat ~mode:0 ~sw t.fd path) Low_level.Open_flags.rdonly in
9454
(Flow.of_fd fd :> Eio.File.ro_ty Eio.Resource.t)
9555

96-
let rec open_out t ~sw ~append ~create path =
56+
let open_out t ~sw ~append ~create path =
9757
let mode, flags =
9858
match create with
9959
| `Never -> 0, Low_level.Open_flags.empty
@@ -102,73 +62,44 @@ end = struct
10262
| `Exclusive perm -> perm, Low_level.Open_flags.(creat + excl)
10363
in
10464
let flags = if append then Low_level.Open_flags.(flags + append) else flags in
105-
let flags = Low_level.Open_flags.(flags + rdwr + opt_nofollow t) in
106-
match
107-
with_parent_dir t path @@ fun dirfd path ->
108-
Low_level.openat ?dirfd ~sw ~mode path flags
109-
with
65+
let flags = Low_level.Open_flags.(flags + rdwr) in
66+
match Low_level.openat ~sw ~mode t.fd path flags with
11067
| fd -> (Flow.of_fd fd :> Eio.File.rw_ty r)
111-
| exception Unix.Unix_error (ELOOP, _, _) ->
112-
(* The leaf was a symlink (or we're unconfined and the main path changed, but ignore that).
113-
A leaf symlink might be OK, but we need to check it's still in the sandbox.
114-
todo: possibly we should limit the number of redirections here, like the kernel does. *)
115-
let target = Unix.readlink path in
116-
let full_target =
117-
if Filename.is_relative target then
118-
Filename.concat (Filename.dirname path) target
119-
else target
120-
in
121-
open_out t ~sw ~append ~create full_target
12268
| exception Unix.Unix_error (code, name, arg) ->
12369
raise (Err.wrap code name arg)
12470

12571
let mkdir t ~perm path =
126-
with_parent_dir t path @@ fun dirfd path ->
127-
Err.run (Low_level.mkdir ?dirfd ~mode:perm) path
72+
Err.run (Low_level.mkdir ~mode:perm t.fd) path
12873

12974
let unlink t path =
130-
with_parent_dir t path @@ fun dirfd path ->
131-
Err.run (Low_level.unlink ?dirfd ~dir:false) path
75+
Err.run (Low_level.unlink ~dir:false t.fd) path
13276

13377
let rmdir t path =
134-
with_parent_dir t path @@ fun dirfd path ->
135-
Err.run (Low_level.unlink ?dirfd ~dir:true) path
78+
Err.run (Low_level.unlink ~dir:true t.fd) path
13679

13780
let stat t ~follow path =
13881
let buf = Low_level.create_stat () in
139-
if follow then (
140-
Err.run (Low_level.fstatat ~buf ~follow:true) (resolve t path);
141-
) else (
142-
with_parent_dir t path @@ fun dirfd path ->
143-
Err.run (Low_level.fstatat ~buf ?dirfd ~follow:false) path;
144-
);
82+
Err.run (Low_level.fstatat ~buf ~follow t.fd) path;
14583
Flow.eio_of_stat buf
14684

14785
let read_dir t path =
148-
(* todo: need fdopendir here to avoid races *)
149-
let path = resolve t path in
150-
Err.run Low_level.readdir path
86+
Err.run (Low_level.readdir t.fd) path
15187
|> Array.to_list
15288

15389
let read_link t path =
154-
with_parent_dir t path @@ fun dirfd path ->
155-
Err.run (Low_level.read_link ?dirfd) path
90+
Err.run (Low_level.read_link t.fd) path
15691

15792
let rename t old_path new_dir new_path =
158-
match Handler.as_posix_dir new_dir with
93+
match as_posix_dir new_dir with
15994
| None -> invalid_arg "Target is not an eio_posix directory!"
160-
| Some new_dir ->
161-
with_parent_dir t old_path @@ fun old_dir old_path ->
162-
with_parent_dir new_dir new_path @@ fun new_dir new_path ->
163-
Err.run (Low_level.rename ?old_dir old_path ?new_dir) new_path
164-
165-
let close t = t.closed <- true
95+
| Some new_dir -> Err.run (Low_level.rename t.fd old_path new_dir) new_path
16696

16797
let open_dir t ~sw path =
168-
Switch.check sw;
98+
let flags = Low_level.Open_flags.(rdonly + directory +? path) in
99+
let fd = Err.run (Low_level.openat ~sw ~mode:0 t.fd path) flags in
169100
let label = Filename.basename path in
170-
let d = v ~label (resolve t path) ~sandbox:true in
171-
Switch.on_release sw (fun () -> close d);
101+
let full_path = if Filename.is_relative path then Filename.concat t.dir_path path else path in
102+
let d = v ~label ~path:full_path (Fd fd) in
172103
Eio.Resource.T (d, Handler.v)
173104

174105
let pp f t = Fmt.string f (String.escaped t.label)
@@ -190,24 +121,13 @@ end = struct
190121
end
191122
and Handler : sig
192123
val v : (Dir.t, [`Dir | `Close]) Eio.Resource.handler
193-
194-
val as_posix_dir : [> `Dir] r -> Dir.t option
195124
end = struct
196-
(* When renaming, we get a plain [Eio.Fs.dir]. We need extra access to check
197-
that the new location is within its sandbox. *)
198-
type (_, _, _) Eio.Resource.pi += Posix_dir : ('t, 't -> Dir.t, [> `Posix_dir]) Eio.Resource.pi
199-
200-
let as_posix_dir (Eio.Resource.T (t, ops)) =
201-
match Eio.Resource.get_opt ops Posix_dir with
202-
| None -> None
203-
| Some fn -> Some (fn t)
204-
205125
let v = Eio.Resource.handler [
206126
H (Eio.Fs.Pi.Dir, (module Dir));
207-
H (Posix_dir, Fun.id);
127+
H (Posix_dir, Dir.fd);
208128
]
209129
end
210130

211131
(* Full access to the filesystem. *)
212-
let fs = Eio.Resource.T (Dir.v ~label:"fs" ~sandbox:false ".", Handler.v)
213-
let cwd = Eio.Resource.T (Dir.v ~label:"cwd" ~sandbox:true ".", Handler.v)
132+
let fs = Eio.Resource.T (Dir.v ~label:"fs" ~path:"." Fs, Handler.v)
133+
let cwd = Eio.Resource.T (Dir.v ~label:"cwd" ~path:"." Cwd, Handler.v)

0 commit comments

Comments
 (0)