Initial FS abstraction

This commit is contained in:
Thomas Leonard 2021-06-15 11:30:41 +01:00
parent f30603d962
commit c18edc3ce4
9 changed files with 582 additions and 3 deletions

View File

@ -21,6 +21,7 @@ unreleased repository.
* [Performance](#performance)
* [Networking](#networking)
* [Design note: object capabilities](#design-note-object-capabilities)
* [Filesystem access](#filesystem-access)
* [Multicore support](#multicore-support)
* [Design note: thread-safety](#design-note-thread-safety)
* [Design note: determinism](#design-note-determinism)
@ -432,6 +433,82 @@ However, it still makes non-malicious code easier to understand and test,
and may allow for an ocap extension to the language in the future.
See [Emily][] for a previous attempt at this.
## Filesystem access
Access to the filesystem is also controlled by capabilities, and `env` provides two:
- `fs` provides full access (just like OCaml's stdlib).
- `cwd` restricts access to files beneath the current working directory.
For example:
```ocaml
let try_write_file dir path data =
match
Switch.top @@ fun sw ->
let flow = Eio.Dir.open_out dir ~sw path ~create:(`Exclusive 0o600) in
Eio.Flow.copy_string data flow
with
| () -> traceln "write %S -> ok" path
| exception ex -> traceln "write %S -> %a" path Fmt.exn ex
let try_mkdir dir path =
match Eio.Dir.mkdir dir path ~perm:0o700 with
| () -> traceln "mkdir %S -> ok" path
| exception ex -> traceln "mkdir %S -> %a" path Fmt.exn ex
```
```ocaml
# Eio_main.run @@ fun env ->
let cwd = Eio.Stdenv.cwd env in
try_mkdir cwd "dir1";
try_mkdir cwd "../dir2";
try_mkdir cwd "/tmp/dir3";
mkdir "dir1" -> ok
mkdir "../dir2" -> Eio.Dir.Permission_denied("..", _)
mkdir "/tmp/dir3" -> Eio.Dir.Permission_denied("/tmp", _)
- : unit = ()
```
The checks also apply to following symlinks:
```ocaml
# Unix.symlink "dir1" "link-to-dir1"; Unix.symlink "/tmp" "link-to-tmp";;
- : unit = ()
# Eio_main.run @@ fun env ->
let cwd = Eio.Stdenv.cwd env in
try_write_file cwd "dir1/file1" "A";
try_write_file cwd "link-to-dir1/file2" "B";
try_write_file cwd "link-to-tmp/file3" "C"
write "dir1/file1" -> ok
write "link-to-dir1/file2" -> ok
write "link-to-tmp/file3" -> Eio.Dir.Permission_denied("link-to-tmp/file3", _)
- : unit = ()
```
You can use `open_dir` to create a restricted capability to a sub-directory:
```ocaml
# Eio_main.run @@ fun env ->
Switch.top @@ fun sw ->
let cwd = Eio.Stdenv.cwd env in
let dir1 = Eio.Dir.open_dir ~sw cwd "dir1" in
try_write_file dir1 "file4" "D";
try_write_file dir1 "../file5" "E"
write "file4" -> ok
write "../file5" -> Eio.Dir.Permission_denied("../file5", _)
- : unit = ()
```
Note that you only need to use `open_dir` if you want to create a new sandboxed environment.
You can use a single directory object to access all paths beneath it,
and this allows following symlinks within that sub-tree.
A program that operates on the current directory will probably want to use `cwd`,
whereas a program that accepts a path from the user will probably want to use `fs`,
perhaps with `open_dir` to constrain all access to be within that directory.
## Multicore support
Fibres are scheduled cooperatively within a single domain, but you can also create new domains that run in parallel.

View File

@ -177,6 +177,34 @@ module Time = struct
let sleep ?sw (t : #clock) d = t#sleep ?sw d
end
module Dir = struct
type path = string
exception Permission_denied of path * exn
type create = [`Never | `If_missing of Unix.file_perm | `Or_truncate of Unix.file_perm | `Exclusive of Unix.file_perm]
class virtual t = object
method virtual open_in : sw:Switch.t -> path -> <Flow.source; Flow.close>
method virtual open_out :
sw:Switch.t ->
append:bool ->
create:create ->
path -> <Flow.two_way; Flow.close>
method virtual mkdir : ?sw:Switch.t -> perm:Unix.file_perm -> path -> unit
method virtual open_dir : sw:Switch.t -> path -> t_with_close
end
and virtual t_with_close = object
(* This dummy class avoids an "Error: The type < .. > is not an object type" error from the compiler. *)
inherit t
method virtual close : unit
end
let open_in ~sw (t:#t) = t#open_in ~sw
let open_out ~sw ?(append=false) ~create (t:#t) path = t#open_out ~sw ~append ~create path
let open_dir ~sw (t:#t) = t#open_dir ~sw
let mkdir ?sw (t:#t) = t#mkdir ?sw
end
module Stdenv = struct
type t = <
@ -186,6 +214,8 @@ module Stdenv = struct
network : Network.t;
domain_mgr : Domain_manager.t;
clock : Time.clock;
fs : Dir.t;
cwd : Dir.t;
>
let stdin (t : <stdin : #Flow.source; ..>) = t#stdin
@ -194,6 +224,8 @@ module Stdenv = struct
let network (t : <network : #Network.t; ..>) = t#network
let domain_mgr (t : <domain_mgr : #Domain_manager.t; ..>) = t#domain_mgr
let clock (t : <clock : #Time.clock; ..>) = t#clock
let fs (t : <fs : #Dir.t; ..>) = t#fs
let cwd (t : <cwd : #Dir.t; ..>) = t#cwd
end
module Private = struct

View File

@ -373,6 +373,57 @@ module Time : sig
@param sw The sleep is aborted if the switch is turned off. *)
end
module Dir : sig
type path = string
exception Permission_denied of path * exn
type create = [`Never | `If_missing of Unix.file_perm | `Or_truncate of Unix.file_perm | `Exclusive of Unix.file_perm]
(** When to create a new file:
If [`Never] then it's an error if the named file doesn't exist.
If [`If_missing] then an existing file is simply opened.
If [`Or_truncate] then an existing file truncated to zero length.
If [`Exclusive] then it is an error is the file does exist.
If a new file is created, the given permissions are used for it. *)
(** A [Dir.t] represents access to a directory and contents, recursively. *)
class virtual t : object
method virtual open_in : sw:Switch.t -> path -> <Flow.source; Flow.close>
method virtual open_out :
sw:Switch.t ->
append:bool ->
create:create ->
path -> <Flow.two_way; Flow.close>
method virtual mkdir : ?sw:Switch.t -> perm:Unix.file_perm -> path -> unit
method virtual open_dir : sw:Switch.t -> path -> t_with_close
end
and virtual t_with_close : object
inherit t
method virtual close : unit
end
val open_in : sw:Switch.t -> #t -> path -> <Flow.source; Flow.close>
(** [open_in ~sw t path] opens [t/path] for reading.
Note: files are always opened in binary mode. *)
val open_out :
sw:Switch.t ->
?append:bool ->
create:create ->
#t -> path -> <Flow.two_way; Flow.close>
(** [open_out ~sw t path] opens [t/path] for reading and writing.
Note: files are always opened in binary mode.
@param append Open for appending: always write at end of file.
@param create Controls whether to create the file, and what permissions to give it if so. *)
val mkdir : ?sw:Switch.t -> #t -> perm:Unix.file_perm -> path -> unit
(** [mkdir t ~perm path] creates a new directory [t/path] with permissions [perm]. *)
val open_dir : sw:Switch.t -> #t -> path -> <t; Flow.close>
(** [open_dir ~sw t path] opens [t/path].
This can be passed to functions to grant access only to the subtree [t/path]. *)
end
(** The standard environment of a process. *)
module Stdenv : sig
type t = <
@ -382,6 +433,8 @@ module Stdenv : sig
network : Network.t;
domain_mgr : Domain_manager.t;
clock : Time.clock;
fs : Dir.t;
cwd : Dir.t;
>
val stdin : <stdin : #Flow.source as 'a; ..> -> 'a
@ -391,6 +444,17 @@ module Stdenv : sig
val network : <network : #Network.t as 'a; ..> -> 'a
val domain_mgr : <domain_mgr : #Domain_manager.t as 'a; ..> -> 'a
val clock : <clock : #Time.clock as 'a; ..> -> 'a
val cwd : <cwd : #Dir.t as 'a; ..> -> 'a
(** [cwd t] is the current working directory of the process (this may change
over time if the process does a `chdir` operation, which is not recommended). *)
val fs : <fs : #Dir.t as 'a; ..> -> 'a
(** [fs t] is the process's full access to the filesystem.
Paths can be absolute or relative (to the current working directory).
Using relative paths with this is similar to using them with {!cwd},
except that this will follow symlinks to other parts of the filesystem.
[fs] is useful for handling paths passed in by the user. *)
end
(** {1 Provider API for OS schedulers} *)

View File

@ -1,4 +1,7 @@
(library
(name eio_linux)
(public_name eio_linux)
(foreign_stubs
(language c)
(names eio_stubs))
(libraries eio eunix unix uring logs fmt bigstringaf ctf))

View File

@ -96,6 +96,7 @@ type io_job =
| Read : rw_req * cancel_hook -> io_job
| Poll_add : int Suspended.t * cancel_hook -> io_job
| Splice : int Suspended.t * cancel_hook -> io_job
| Openat2 : int Suspended.t * cancel_hook -> io_job
| Connect : int Suspended.t * cancel_hook -> io_job
| Accept : int Suspended.t * cancel_hook -> io_job
| Close : int Suspended.t -> io_job
@ -244,6 +245,17 @@ let rec enqueue_splice ?sw st action ~src ~dst ~len =
if retry then (* wait until an sqe is available *)
Queue.push (fun st -> enqueue_splice ?sw st action ~src ~dst ~len) st.io_q
let rec enqueue_openat2 st action ((sw, access, flags, perm, resolve, dir, path) as args) =
Log.debug (fun l -> l "openat2: submitting call");
Ctf.label "openat2";
let fd = Option.map (FD.get "openat2") dir in
let retry = with_cancel_hook ~sw ~action st (fun cancel ->
Uring.openat2 st.uring ~access ~flags ~perm ~resolve ?fd path (Openat2 (action, cancel))
)
in
if retry then (* wait until an sqe is available *)
Queue.push (fun st -> enqueue_openat2 st action args) st.io_q
let rec enqueue_connect ?sw st action fd addr =
Log.debug (fun l -> l "connect: submitting call");
Ctf.label "connect";
@ -348,6 +360,10 @@ and handle_complete st ~runnable result =
Log.debug (fun l -> l "connect returned");
Switch.remove_hook !cancel;
Suspended.continue k result
| Openat2 (k, cancel) ->
Log.debug (fun l -> l "openat2 returned");
Switch.remove_hook !cancel;
Suspended.continue k result
| Accept (k, cancel) ->
Log.debug (fun l -> l "accept returned");
Switch.remove_hook !cancel;
@ -378,7 +394,7 @@ and complete_rw_req st ({len; cur_off; action; _} as req) res =
let alloc_buf st k =
Log.debug (fun l -> l "alloc: %d" (Uring.Region.avail st.mem));
match Uring.Region.alloc st.mem with
| buf -> Suspended.continue k buf
| buf -> Suspended.continue k buf
| exception Uring.Region.No_space ->
Queue.push k st.mem_q;
schedule st
@ -473,9 +489,48 @@ let openfile ~sw path flags mode =
let fd = Unix.openfile path flags mode in
FD.of_unix ~sw ~seekable:(FD.is_seekable fd) fd
effect Openat2 : (Switch.t * [`R|`W|`RW] * Uring.Open_flags.t * Unix.file_perm * Uring.Resolve.t * FD.t option * string) -> int
let openat2 ~sw ?seekable ~access ~flags ~perm ~resolve ?dir path =
let res = perform (Openat2 (sw, access, flags, perm, resolve, dir, path)) in
if res < 0 then (
Switch.check sw; (* If cancelled, report that instead. *)
let ex = Unix.Unix_error (Uring.error_of_errno res, "openat2", "") in
if res = -18 then raise (Eio.Dir.Permission_denied (path, ex))
else raise ex
);
let fd : Unix.file_descr = Obj.magic res in
let seekable =
match seekable with
| None -> FD.is_seekable fd
| Some x -> x
in
FD.of_unix ~sw ~seekable fd
let fstat fd =
Unix.fstat (FD.get "fstat" fd)
external eio_mkdirat : Unix.file_descr -> string -> Unix.file_perm -> unit = "caml_eio_mkdirat"
(* We ignore [sw] because this isn't a uring operation yet. *)
let mkdirat ?sw:_ ~perm dir path =
match dir with
| None -> Unix.mkdir path perm
| Some dir -> eio_mkdirat (FD.get "mkdirat" dir) path perm
let mkdir_beneath ?sw ~perm ?dir path =
let dir_path = Filename.dirname path in
let leaf = Filename.basename path in
(* [mkdir] is really an operation on [path]'s parent. Get a reference to that first: *)
Switch.sub_opt sw (fun sw ->
let parent = openat2 ~sw ~seekable:false ?dir dir_path
~access:`R
~flags:Uring.Open_flags.(cloexec + path + directory)
~perm:0
~resolve:Uring.Resolve.beneath
in
mkdirat ~sw ~perm (Some parent) leaf
)
let shutdown socket command =
Unix.shutdown (FD.get "shutdown" socket) command
@ -642,6 +697,8 @@ module Objects = struct
network : Eio.Network.t;
domain_mgr : Eio.Domain_manager.t;
clock : Eio.Time.clock;
fs : Eio.Dir.t;
cwd : Eio.Dir.t;
>
let domain_mgr = object
@ -668,11 +725,69 @@ module Objects = struct
method sleep ?sw d = sleep ?sw d
end
class dir fd = object
inherit Eio.Dir.t
val resolve_flags = Uring.Resolve.beneath
method open_in ~sw path =
let fd = openat2 ~sw ?dir:fd path
~access:`R
~flags:Uring.Open_flags.cloexec
~perm:0
~resolve:Uring.Resolve.beneath
in
(flow fd :> <Eio.Flow.source; Eio.Flow.close>)
method open_out ~sw ~append ~create path =
let perm, flags =
match create with
| `Never -> 0, Uring.Open_flags.empty
| `If_missing perm -> perm, Uring.Open_flags.creat
| `Or_truncate perm -> perm, Uring.Open_flags.(creat + trunc)
| `Exclusive perm -> perm, Uring.Open_flags.(creat + excl)
in
let flags = if append then Uring.Open_flags.(flags + append) else flags in
let fd = openat2 ~sw ?dir:fd path
~access:`RW
~flags:Uring.Open_flags.(cloexec + flags)
~perm
~resolve:resolve_flags
in
(flow fd :> <Eio.Flow.two_way; Eio.Flow.close>)
method open_dir ~sw path =
let fd = openat2 ~sw ~seekable:false ?dir:fd path
~access:`R
~flags:Uring.Open_flags.(cloexec + path + directory)
~perm:0
~resolve:resolve_flags
in
(new dir (Some fd) :> <Eio.Dir.t; Eio.Flow.close>)
method mkdir ?sw ~perm path =
mkdir_beneath ?sw ~perm ?dir:fd path
method close =
FD.close (Option.get fd)
end
(* Full access to the filesystem. *)
let fs = object
inherit dir None
val! resolve_flags = Uring.Resolve.empty
method! mkdir ?sw ~perm path =
mkdirat ?sw ~perm None path
end
let stdenv () =
let of_unix fd = FD.of_unix_no_hook ~seekable:(FD.is_seekable fd) fd in
let stdin = lazy (source (of_unix Unix.stdin)) in
let stdout = lazy (sink (of_unix Unix.stdout)) in
let stderr = lazy (sink (of_unix Unix.stderr)) in
let cwd = new dir None in
object (_ : stdenv)
method stdin = Lazy.force stdin
method stdout = Lazy.force stdout
@ -680,6 +795,8 @@ module Objects = struct
method network = network
method domain_mgr = domain_mgr
method clock = clock
method fs = (fs :> Eio.Dir.t)
method cwd = (cwd :> Eio.Dir.t)
end
end
@ -695,7 +812,7 @@ let run ?(queue_depth=64) ?(block_size=4096) main =
(* TODO unify this allocation API around baregion/uring *)
let fixed_buf_len = block_size * queue_depth in
let uring = Uring.create ~fixed_buf_len ~queue_depth () in
let buf = Uring.buf uring in
let buf = Uring.buf uring in
let mem = Uring.Region.init ~block_size buf queue_depth in
let run_q = Queue.create () in
let sleep_q = Zzz.create () in
@ -727,6 +844,10 @@ let run ?(queue_depth=64) ?(block_size=4096) main =
let k = { Suspended.k; tid } in
enqueue_splice ?sw st k ~src ~dst ~len;
schedule st
| effect (Openat2 args) k ->
let k = { Suspended.k; tid } in
enqueue_openat2 st k args;
schedule st
| effect (Connect (sw, fd, addr)) k ->
let k = { Suspended.k; tid } in
enqueue_connect ?sw st k fd addr;

View File

@ -62,6 +62,17 @@ val with_chunk : (Uring.Region.chunk -> 'a) -> 'a
val openfile : sw:Switch.t -> string -> Unix.open_flag list -> int -> FD.t
(** Like {!Unix.open_file}. *)
val openat2 :
sw:Switch.t ->
?seekable:bool ->
access:[`R|`W|`RW] ->
flags:Uring.Open_flags.t ->
perm:Unix.file_perm ->
resolve:Uring.Resolve.t ->
?dir:FD.t -> string -> FD.t
(** [openat2 ~sw ~flags ~perm ~resolve ~dir path] opens [dir/path].
See {!Uring.openat2} for details. *)
val read_upto : ?sw:Switch.t -> ?file_offset:Optint.Int63.t -> FD.t -> Uring.Region.chunk -> int -> int
(** [read_upto fd chunk len] reads at most [len] bytes from [fd],
returning as soon as some data is available.
@ -124,6 +135,8 @@ module Objects : sig
network : Eio.Network.t;
domain_mgr : Eio.Domain_manager.t;
clock : Eio.Time.clock;
fs : Eio.Dir.t;
cwd : Eio.Dir.t;
>
val get_fd : <has_fd; ..> -> FD.t

21
lib_eio_linux/eio_stubs.c Normal file
View File

@ -0,0 +1,21 @@
#include <sys/stat.h>
#include <sys/types.h>
#include <caml/mlvalues.h>
#include <caml/memory.h>
#include <caml/signals.h>
#include <caml/unixsupport.h>
CAMLprim value caml_eio_mkdirat(value v_fd, value v_path, value v_perm) {
CAMLparam1(v_path);
char *path;
int ret;
caml_unix_check_path(v_path, "mkdirat");
path = caml_stat_strdup(String_val(v_path));
caml_enter_blocking_section();
ret = mkdirat(Int_val(v_fd), path, Int_val(v_perm));
caml_leave_blocking_section();
caml_stat_free(path);
if (ret == -1) uerror("mkdirat", v_path);
CAMLreturn(Val_unit);
}

@ -1 +1 @@
Subproject commit 31b098549f2017852b28f6821eb9e8657672850c
Subproject commit f73cc8c73c7bd26ccb3d7a628150be1898c29ad6

248
tests/test_fs.md Normal file
View File

@ -0,0 +1,248 @@
# Setting up the environment
```ocaml
# #require "eio_main";;
# ignore @@ Unix.umask 0o022;;
- : unit = ()
```
```ocaml
open Eio.Std
let run (fn : sw:Switch.t -> Eio.Stdenv.t -> unit) =
try
Eio_main.run @@ fun env ->
Switch.top @@ fun sw ->
fn ~sw env;
print_endline "ok"
with
| Failure msg -> print_endline msg
| ex -> print_endline (Printexc.to_string ex)
let read_all ?sw flow =
let b = Buffer.create 100 in
Eio.Flow.copy ?sw flow (Eio.Flow.buffer_sink b);
Buffer.contents b
let write_file ~sw ~create ?append dir path content =
Switch.sub sw ~on_error:raise (fun sw ->
let flow = Eio.Dir.open_out ~sw ~create ?append dir path in
Eio.Flow.copy_string content flow
)
let try_write_file ~sw ~create ?append dir path content =
match write_file ~sw ~create ?append dir path content with
| () -> traceln "write %S -> ok" path
| exception ex -> traceln "write %S -> %a" path Fmt.exn ex
let read_file ~sw dir path =
Switch.sub sw ~on_error:raise (fun sw ->
let flow = Eio.Dir.open_in ~sw dir path in
read_all flow
)
let try_mkdir dir path =
match Eio.Dir.mkdir dir path ~perm:0o700 with
| () -> traceln "mkdir %S -> ok" path
| exception ex -> traceln "mkdir %S -> %a" path Fmt.exn ex
let chdir path =
traceln "chdir %S" path;
Unix.chdir path
```
# Basic test cases
Creating a file and reading it back:
```ocaml
# run @@ fun ~sw env ->
let cwd = Eio.Stdenv.cwd env in
write_file ~sw ~create:(`Exclusive 0o666) cwd "test-file" "my-data";
traceln "Got %S" @@ read_file ~sw cwd "test-file"
Got "my-data"
ok
- : unit = ()
```
Check the file got the correct permissions (subject to the umask set above):
```ocaml
# traceln "Perm = %o" ((Unix.stat "test-file").st_perm);;
Perm = 644
- : unit = ()
```
# Sandboxing
Trying to use cwd to access a file outside of that subtree fails:
```ocaml
# run @@ fun ~sw env ->
let cwd = Eio.Stdenv.cwd env in
write_file ~sw ~create:(`Exclusive 0o666) cwd "../test-file" "my-data";
failwith "Should have failed"
Eio.Dir.Permission_denied("../test-file", _)
- : unit = ()
```
Trying to use cwd to access an absolute path fails:
```ocaml
# run @@ fun ~sw env ->
let cwd = Eio.Stdenv.cwd env in
write_file ~sw ~create:(`Exclusive 0o666) cwd "/tmp/test-file" "my-data";
failwith "Should have failed"
Eio.Dir.Permission_denied("/tmp/test-file", _)
- : unit = ()
```
# Creation modes
Exclusive create fails if already exists:
```ocaml
# run @@ fun ~sw env ->
let cwd = Eio.Stdenv.cwd env in
write_file ~sw ~create:(`Exclusive 0o666) cwd "test-file" "first-write";
write_file ~sw ~create:(`Exclusive 0o666) cwd "test-file" "first-write";
failwith "Should have failed"
Unix.Unix_error(Unix.EEXIST, "openat2", "")
- : unit = ()
```
If-missing create succeeds if already exists:
```ocaml
# run @@ fun ~sw env ->
let cwd = Eio.Stdenv.cwd env in
write_file ~sw ~create:(`If_missing 0o666) cwd "test-file" "1st-write-original";
write_file ~sw ~create:(`If_missing 0o666) cwd "test-file" "2nd-write";
traceln "Got %S" @@ read_file ~sw cwd "test-file"
Got "2nd-write-original"
ok
- : unit = ()
```
Truncate create succeeds if already exists, and truncates:
```ocaml
# run @@ fun ~sw env ->
let cwd = Eio.Stdenv.cwd env in
write_file ~sw ~create:(`Or_truncate 0o666) cwd "test-file" "1st-write-original";
write_file ~sw ~create:(`Or_truncate 0o666) cwd "test-file" "2nd-write";
traceln "Got %S" @@ read_file ~sw cwd "test-file"
Got "2nd-write"
ok
- : unit = ()
# Unix.unlink "test-file";;
- : unit = ()
```
Error if no create and doesn't exist:
```ocaml
# run @@ fun ~sw env ->
let cwd = Eio.Stdenv.cwd env in
write_file ~sw ~create:`Never cwd "test-file" "1st-write-original";
traceln "Got %S" @@ read_file ~sw cwd "test-file"
Unix.Unix_error(Unix.ENOENT, "openat2", "")
- : unit = ()
```
Appending to an existing file:
```ocaml
# run @@ fun ~sw env ->
let cwd = Eio.Stdenv.cwd env in
write_file ~sw ~create:(`Or_truncate 0o666) cwd "test-file" "1st-write-original";
write_file ~sw ~create:`Never ~append:true cwd "test-file" "2nd-write";
traceln "Got %S" @@ read_file ~sw cwd "test-file"
Got "1st-write-original2nd-write"
ok
- : unit = ()
# Unix.unlink "test-file";;
- : unit = ()
```
# Mkdir
```ocaml
# run @@ fun ~sw env ->
let cwd = Eio.Stdenv.cwd env in
try_mkdir cwd "subdir";
try_mkdir cwd "subdir/nested";
write_file ~sw ~create:(`Exclusive 0o600) cwd "subdir/nested/test-file" "data";
()
mkdir "subdir" -> ok
mkdir "subdir/nested" -> ok
ok
- : unit = ()
# Unix.unlink "subdir/nested/test-file"; Unix.rmdir "subdir/nested"; Unix.rmdir "subdir";;
- : unit = ()
```
Creating directories with nesting, symlinks, etc:
```ocaml
# Unix.symlink "/" "to-root";;
- : unit = ()
# Unix.symlink "subdir" "to-subdir";;
- : unit = ()
# Unix.symlink "foo" "dangle";;
- : unit = ()
# run @@ fun ~sw env ->
let cwd = Eio.Stdenv.cwd env in
try_mkdir cwd "subdir";
try_mkdir cwd "to-subdir/nested";
try_mkdir cwd "to-root/tmp/foo";
try_mkdir cwd "../foo";
try_mkdir cwd "to-subdir";
try_mkdir cwd "dangle/foo";
()
mkdir "subdir" -> ok
mkdir "to-subdir/nested" -> ok
mkdir "to-root/tmp/foo" -> Eio.Dir.Permission_denied("to-root/tmp", _)
mkdir "../foo" -> Eio.Dir.Permission_denied("..", _)
mkdir "to-subdir" -> Unix.Unix_error(Unix.EEXIST, "mkdirat", "to-subdir")
mkdir "dangle/foo" -> Unix.Unix_error(Unix.ENOENT, "openat2", "")
ok
- : unit = ()
```
# Limiting to a subdirectory
Create a sandbox, write a file with it, then read it from outside:
```ocaml
# run @@ fun ~sw env ->
let cwd = Eio.Stdenv.cwd env in
try_mkdir cwd "sandbox";
let subdir = Eio.Dir.open_dir ~sw cwd "sandbox" in
write_file ~sw ~create:(`Exclusive 0o600) subdir "test-file" "data";
try_mkdir subdir "../new-sandbox";
traceln "Got %S" @@ read_file ~sw cwd "sandbox/test-file"
mkdir "sandbox" -> ok
mkdir "../new-sandbox" -> Eio.Dir.Permission_denied("..", _)
Got "data"
ok
- : unit = ()
```
# Unconfined FS access
We create a directory and chdir into it.
Using `cwd` we can't access the parent, but using `fs` we can:
```ocaml
# run @@ fun ~sw env ->
let cwd = Eio.Stdenv.cwd env in
let fs = Eio.Stdenv.fs env in
try_mkdir cwd "fs-test";
chdir "fs-test";
Fun.protect ~finally:(fun () -> chdir "..") (fun () ->
try_mkdir cwd "../outside-cwd";
try_write_file ~sw ~create:(`Exclusive 0o600) cwd "../test-file" "data";
try_mkdir fs "../outside-cwd";
try_write_file ~sw ~create:(`Exclusive 0o600) fs "../test-file" "data";
);
Unix.unlink "test-file";
Unix.rmdir "outside-cwd"
mkdir "fs-test" -> ok
chdir "fs-test"
mkdir "../outside-cwd" -> Eio.Dir.Permission_denied("..", _)
write "../test-file" -> Eio.Dir.Permission_denied("../test-file", _)
mkdir "../outside-cwd" -> ok
write "../test-file" -> ok
chdir ".."
ok
- : unit = ()
```