Skip to content

Commit 4041650

Browse files
committed
Initial symlink sandboxing support
1 parent 82532df commit 4041650

File tree

6 files changed

+108
-27
lines changed

6 files changed

+108
-27
lines changed

lib_eio_windows/eio_windows_stubs.c

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,23 @@ CAMLprim value caml_eio_windows_pwritev(value v_fd, value v_bufs, value v_offset
8383

8484
// File-system operations
8585

86+
// No follow
87+
void no_follow(HANDLE h) {
88+
BY_HANDLE_FILE_INFORMATION b;
89+
90+
if (!GetFileInformationByHandle(h, &b)) {
91+
caml_win32_maperr(GetLastError());
92+
uerror("nofollow", Nothing);
93+
}
94+
95+
if (b.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {
96+
CloseHandle(h);
97+
caml_unix_error(ELOOP, "nofollow", Nothing);
98+
}
99+
}
100+
86101
// We recreate an openat like function using NtCreateFile
87-
CAMLprim value caml_eio_windows_openat(value v_dirfd, value v_pathname, value v_desired_access, value v_create_disposition, value v_create_options)
102+
CAMLprim value caml_eio_windows_openat(value v_dirfd, value v_nofollow, value v_pathname, value v_desired_access, value v_create_disposition, value v_create_options)
88103
{
89104
CAMLparam2(v_dirfd, v_pathname);
90105
HANDLE h, dir;
@@ -121,14 +136,17 @@ CAMLprim value caml_eio_windows_openat(value v_dirfd, value v_pathname, value v_
121136
// Create the file
122137
r = NtCreatefile(
123138
&h,
124-
Int_val(v_desired_access),
139+
Int_val(v_desired_access) | FILE_READ_ATTRIBUTES,
125140
&obj_attr,
126141
&io_status,
127142
0, // Allocation size
128143
FILE_ATTRIBUTE_NORMAL, // TODO: Could check flags to see if we can do READONLY here a la OCaml
129144
(FILE_SHARE_READ | FILE_SHARE_WRITE),
130145
Int_val(v_create_disposition),
131-
(Int_val(v_create_options) | FILE_SYNCHRONOUS_IO_NONALERT),
146+
(
147+
FILE_SYNCHRONOUS_IO_NONALERT
148+
| FILE_OPEN_FOR_BACKUP_INTENT
149+
| (Bool_val(v_nofollow) ? FILE_FLAG_OPEN_REPARSE_POINT : Int_val(v_create_options))),
132150
NULL, // Extended attribute buffer
133151
0 // Extended attribute buffer length
134152
);
@@ -138,17 +156,28 @@ CAMLprim value caml_eio_windows_openat(value v_dirfd, value v_pathname, value v_
138156

139157
if (h == INVALID_HANDLE_VALUE) {
140158
caml_win32_maperr(RtlNtStatusToDosError(r));
141-
uerror("openat", v_pathname);
159+
uerror("openat handle", v_pathname);
142160
}
143161

144-
if (!NT_SUCCESS(r)) {
162+
if (!NT_SUCCESS(r)) {
145163
caml_win32_maperr(RtlNtStatusToDosError(r));
146164
uerror("openat", Nothing);
147165
}
166+
167+
// No follow check -- Windows doesn't actually have that ability
168+
// so we have to do it after the fact. This will raise if a symbolic
169+
// link is encountered and will close the handle.
170+
if (Bool_val(v_nofollow)) {
171+
no_follow(h);
172+
}
148173

149174
CAMLreturn(caml_win32_alloc_handle(h));
150175
}
151176

177+
value caml_eio_windows_openat_bytes(value* values, int argc) {
178+
return caml_eio_windows_openat(values[0], values[1], values[2], values[3], values[4], values[5]);
179+
}
180+
152181
CAMLprim value caml_eio_windows_unlinkat(value v_dirfd, value v_pathname, value v_dir)
153182
{
154183
CAMLparam2(v_dirfd, v_pathname);

lib_eio_windows/fs.ml

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ module Fd = Eio_unix.Fd
2929
class virtual posix_dir = object
3030
inherit Eio.Fs.dir
3131

32-
val virtual opt_nofollow : Low_level.Flags.Open.t
33-
(** Extra flags for open operations. Sandboxes will add [O_NOFOLLOW] here. *)
32+
val virtual opt_nofollow : bool
33+
(** Emulate [O_NOFOLLOW] here. *)
3434

3535
method virtual private resolve : string -> string
3636
(** [resolve path] returns the real path that should be used to access [path].
@@ -60,7 +60,7 @@ class virtual dir ~label = object (self)
6060

6161
method open_in ~sw path =
6262
let open Low_level in
63-
let fd = Err.run (Low_level.openat ~sw (self#resolve path)) Low_level.Flags.Open.(generic_read + synchronise) Flags.Disposition.(open_if) Flags.Create.(non_directory) in
63+
let fd = Err.run (Low_level.openat ~sw ~nofollow:opt_nofollow (self#resolve path)) Low_level.Flags.Open.(generic_read + synchronise) Flags.Disposition.(open_if) Flags.Create.(non_directory) in
6464
(Flow.of_fd fd :> <Eio.File.ro; Eio.Flow.close>)
6565

6666
method open_out ~sw ~append ~create path =
@@ -75,10 +75,12 @@ class virtual dir ~label = object (self)
7575
let flags = if append then Low_level.Flags.Open.(synchronise + append) else Low_level.Flags.Open.(generic_write + synchronise) in
7676
match
7777
self#with_parent_dir path @@ fun dirfd path ->
78-
Low_level.openat ?dirfd ~sw path flags disp Flags.Create.(non_directory)
78+
Low_level.openat ?dirfd ~nofollow:opt_nofollow ~sw path flags disp Flags.Create.(non_directory)
7979
with
8080
| fd -> (Flow.of_fd fd :> <Eio.File.rw; Eio.Flow.close>)
81-
| exception Unix.Unix_error (ELOOP, _, _) ->
81+
(* This is the result of raising [caml_unix_error(ELOOP,...)] *)
82+
| exception Unix.Unix_error (EUNKNOWNERR 114, _, _) ->
83+
print_endline "UNKNOWN";
8284
(* The leaf was a symlink (or we're unconfined and the main path changed, but ignore that).
8385
A leaf symlink might be OK, but we need to check it's still in the sandbox.
8486
todo: possibly we should limit the number of redirections here, like the kernel does. *)
@@ -133,8 +135,7 @@ end
133135
and sandbox ~label dir_path = object (self)
134136
inherit dir ~label
135137

136-
(* nofollow not on windows. *)
137-
val opt_nofollow = Low_level.Flags.Open.empty
138+
val opt_nofollow = true
138139

139140
(* Resolve a relative path to an absolute one, with no symlinks.
140141
@raise Eio.Fs.Permission_denied if it's outside of [dir_path]. *)
@@ -145,9 +146,9 @@ and sandbox ~label dir_path = object (self)
145146
let full = Err.run Low_level.realpath (Filename.concat dir_path path) in
146147
let prefix_len = String.length dir_path + 1 in
147148
(* \\??\\ Is necessary with NtCreateFile. *)
148-
if String.length full >= prefix_len && String.sub full 0 prefix_len = dir_path ^ Filename.dir_sep then
149+
if String.length full >= prefix_len && String.sub full 0 prefix_len = dir_path ^ Filename.dir_sep then begin
149150
"\\??\\" ^ full
150-
else if full = dir_path then
151+
end else if full = dir_path then
151152
"\\??\\" ^ full
152153
else
153154
raise @@ Eio.Fs.err (Permission_denied (Err.Outside_sandbox (full, dir_path)))
@@ -167,7 +168,7 @@ and sandbox ~label dir_path = object (self)
167168
let dir = self#resolve dir in
168169
Switch.run @@ fun sw ->
169170
let open Low_level in
170-
let dirfd = Low_level.openat ~sw dir Flags.Open.(generic_read + synchronise) Flags.Disposition.(open_if) Flags.Create.(directory) in
171+
let dirfd = Low_level.openat ~sw ~nofollow:true dir Flags.Open.(generic_read + synchronise) Flags.Disposition.(open_if) Flags.Create.(directory) in
171172
fn (Some dirfd) leaf
172173
)
173174
end
@@ -176,7 +177,7 @@ end
176177
let fs = object
177178
inherit dir ~label:"fs"
178179

179-
val opt_nofollow = Low_level.Flags.Open.empty
180+
val opt_nofollow = false
180181

181182
(* No checks *)
182183
method private resolve path = path

lib_eio_windows/low_level.ml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -199,17 +199,17 @@ let rec with_dirfd op dirfd fn =
199199
| Some dirfd -> Fd.use_exn op dirfd (fun fd -> fn (Some fd))
200200
| exception Unix.Unix_error(Unix.EINTR, _, "") -> with_dirfd op dirfd fn
201201

202-
external eio_openat : Unix.file_descr option -> string -> Flags.Open.t -> Flags.Disposition.t -> Flags.Create.t -> Unix.file_descr = "caml_eio_windows_openat"
202+
external eio_openat : Unix.file_descr option -> bool -> string -> Flags.Open.t -> Flags.Disposition.t -> Flags.Create.t -> Unix.file_descr = "caml_eio_windows_openat_bytes" "caml_eio_windows_openat"
203203

204-
let openat ?dirfd ~sw path flags dis create =
204+
let openat ?dirfd ?(nofollow=false) ~sw path flags dis create =
205205
with_dirfd "openat" dirfd @@ fun dirfd ->
206206
Switch.check sw;
207-
in_worker_thread (fun () -> eio_openat dirfd path Flags.Open.(flags + cloexec (* + nonblock *)) dis create)
207+
in_worker_thread (fun () -> eio_openat dirfd nofollow path Flags.Open.(flags + cloexec (* + nonblock *)) dis create)
208208
|> Fd.of_unix ~sw ~blocking:false ~close_unix:true
209209

210-
let mkdir ?dirfd ~mode:_ path =
210+
let mkdir ?dirfd ?(nofollow=false) ~mode:_ path =
211211
Switch.run @@ fun sw ->
212-
let _ : Fd.t = openat ?dirfd ~sw path Flags.Open.(generic_write + synchronise) Flags.Disposition.(create) Flags.Create.(directory) in
212+
let _ : Fd.t = openat ?dirfd ~nofollow ~sw path Flags.Open.(generic_write + synchronise) Flags.Disposition.(create) Flags.Create.(directory) in
213213
()
214214

215215
external eio_unlinkat : Unix.file_descr option -> string -> bool -> unit = "caml_eio_windows_unlinkat"

lib_eio_windows/low_level.mli

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ val lstat : string -> Unix.LargeFile.stats
3939

4040
val realpath : string -> string
4141

42-
val mkdir : ?dirfd:fd -> mode:int -> string -> unit
42+
val mkdir : ?dirfd:fd -> ?nofollow:bool -> mode:int -> string -> unit
4343
val unlink : ?dirfd:fd -> dir:bool -> string -> unit
4444
val rename : ?old_dir:fd -> string -> ?new_dir:fd -> string -> unit
4545

@@ -115,5 +115,5 @@ module Flags : sig
115115
end
116116
end
117117

118-
val openat : ?dirfd:fd -> sw:Switch.t -> string -> Flags.Open.t -> Flags.Disposition.t -> Flags.Create.t -> fd
118+
val openat : ?dirfd:fd -> ?nofollow:bool-> sw:Switch.t -> string -> Flags.Open.t -> Flags.Disposition.t -> Flags.Create.t -> fd
119119
(** Note: the returned FD is always non-blocking and close-on-exec. *)

lib_eio_windows/test/test.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ end
5151

5252
let () =
5353
Eio_windows.run @@ fun env ->
54-
Alcotest.run "eio_windows" [
54+
Alcotest.run ~bail:true "eio_windows" [
5555
"net", Test_net.tests env;
5656
"fs", Test_fs.tests env;
5757
"timeout", Timeout.tests env;

lib_eio_windows/test/test_fs.ml

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ let try_write_file ~create ?append path content =
1818
| exception ex -> raise ex
1919

2020
let try_mkdir path =
21+
traceln "mkdir %a -> ?" Path.pp path;
2122
match Path.mkdir path ~perm:0o700 with
2223
| () -> traceln "mkdir %a -> ok" Path.pp path
2324
| exception ex -> raise ex
@@ -71,9 +72,12 @@ let test_cwd_no_access_abs env () =
7172
let test_exclusive env () =
7273
let cwd = Eio.Stdenv.cwd env in
7374
with_temp_file (cwd / "test-file") @@ fun path ->
75+
Eio.traceln "fiest";
7476
Path.save ~create:(`Exclusive 0o666) path "first-write";
77+
Eio.traceln "next";
7578
try
7679
Path.save ~create:(`Exclusive 0o666) path "first-write";
80+
Eio.traceln "nope";
7781
failwith "Should have failed"
7882
with Eio.Io (Eio.Fs.E (Already_exists _), _) -> ()
7983

@@ -121,9 +125,56 @@ let test_mkdir env () =
121125
Unix.rmdir "subdir\\nested";
122126
Unix.rmdir "subdir"
123127

124-
let test_symlink _env () =
125-
(* TODO *)
126-
()
128+
let test_symlink env () =
129+
(*
130+
Important note: assuming that neither "another" nor
131+
"to-subdir" exist, the following program will behave
132+
differently if you don't have the ~to_dir flag.
133+
134+
With [to_dir] set to [true] we get the desired UNIX behaviour,
135+
without it [Unix.realpath] will actually show the parent directory
136+
of "another". Presumably this is because Windows distinguishes
137+
between file symlinks and directory symlinks. Fun.
138+
139+
{[ Unix.symlink ~to_dir:true "another" "to-subdir";
140+
Unix.mkdir "another" 0o700;
141+
print_endline @@ Unix.realpath "to-subdir" |}
142+
*)
143+
let cwd = Eio.Stdenv.cwd env in
144+
try_mkdir (cwd / "sandbox");
145+
Unix.symlink ~to_dir:true ".." "sandbox\\to-root";
146+
Unix.symlink ~to_dir:true "subdir" "sandbox\\to-subdir";
147+
Unix.symlink ~to_dir:true "foo" "sandbox\\dangle";
148+
try_mkdir (cwd / "tmp");
149+
Eio.Path.with_open_dir (cwd / "sandbox") @@ fun sandbox ->
150+
try_mkdir (sandbox / "subdir");
151+
try_mkdir (sandbox / "to-subdir\\nested");
152+
let () =
153+
try
154+
try_mkdir (sandbox / "to-root\\tmp\\foo");
155+
failwith "Expected permission denied to-root"
156+
with Eio.Io (Eio.Fs.E (Permission_denied _), _) -> ()
157+
in
158+
assert (not (Sys.file_exists ".\\tmp\\foo"));
159+
let () =
160+
try
161+
try_mkdir (sandbox / "..\\foo");
162+
failwith "Expected permission denied parent foo"
163+
with Eio.Io (Eio.Fs.E (Permission_denied _), _) -> ()
164+
in
165+
let () =
166+
try
167+
try_mkdir (sandbox / "to-subdir");
168+
failwith "Expected already exists"
169+
with Eio.Io (Eio.Fs.E (Already_exists _), _) -> ()
170+
in
171+
let () =
172+
try
173+
try_mkdir (sandbox / "dangle\\foo");
174+
failwith "Expected permission denied dangle foo"
175+
with Eio.Io (Eio.Fs.E (Not_found _), _) -> ()
176+
in
177+
()
127178

128179
let test_unlink env () =
129180
let cwd = Eio.Stdenv.cwd env in

0 commit comments

Comments
 (0)