[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: [MirageOS-devel] Error handling in Mirage - request for comments!

On 2 February 2015 at 00:45, Christophe Troestler
<Christophe.Troestler@xxxxxxxxxxx> wrote:
> On Sat, 31 Jan 2015 21:07:22 +0000, Thomas Leonard wrote:
>> [â] But the real problem here is that by making exceptions fatal we
>> turn every exception used in existing OCaml code into a security
>> vulnerability. [â]
> Since OCaml supports exceptions, I think there is no doubt that we
> must handle them effectively â not just wipe them under the carpet and
> let them abort the program/unikernel.  Ideally Lwt should deal
> sensibly with exceptions (and the discussion seems to imply there are
> areas of improvement â but it is unclear to me how you can do that
> efficiently without using a try block in all binds).

Lwt already puts a try block in all (sleeping) binds, so this is
handled for you:

let bind t f =
  let t = repr t in
  match t.state with
    | Return v ->
        f v
    | Fail _ as state ->
        thread { state }
    | Sleep sleeper ->
        let res = temp t in
        let data = !current_data in
        add_immutable_waiter sleeper
             | Return v -> current_data := data; connect res (try f v
with exn -> fail exn)
             | Fail _ as state -> fast_connect res state
             | _ -> assert false);

> So it seems to
> me that there is little choice but that exceptions be turned into
> failed threads at the programmer's discretion.  Maybe ârunâ should be
> renamed ârun_exnâ to indicate that exceptions pass through and the
> "safe" run should be
>     run : (unit -> 'a t) -> [`Ok of 'a | `Exn of exn]
> As it was previously mentioned, I also think that using "error-aware
> return types" should be limited mostly to cases where the failure is
> not really an error and should be dealt with immediately by the
> caller.  And I agree with Thomas when he says that overusing this
> scheme will result in errors being ignored instead of being dealt with
> properly.
> There are however errors that you want to mandate that they are dealt
> with but not necessarily at the calling point.  A typical example to
> me is âreadâ because it is often much "higher" in the program that an
> appropriate response to a failing read can be made â this is important
> for libraries that will post-process âreadâs and will have no idea
> what to do if âreadâ fails.

Yes, this is a case where you might want an Or_error.t threaded
through everything. Or, it might be easier to wrap the whole thing so
that any error code returned by the read is thrown and then caught

> To make the discussion a bit more concrete, here is a signature
> illustrating how these latter errors may be handled:
>     type ('a, 'b) t
>     val return : 'a -> ('a, 'b) t
>     val ( >>= ) : ('a, 'b) t -> ('a -> ('c, 'b) t) -> ('c, 'b) t
>     val fail : ([> ] as 'b) -> (_, 'b) t
>     val fail_exn : exn -> ('a, 'b) t
>     val catch : ('a, 'b) t -> ('b -> ('a, 'c) t) -> ('a, 'c) t
>     val catch_exn : ?exn:(exn -> ('a, 'b) t) -> (unit -> ('a, 'b) t) -> ('a, 
> 'b) t
>     val run : (unit -> ('a, unit) t) -> [`Ok of 'a | `Exn of exn]
>     val run_exn : ('a, unit) t -> 'a
> Some remarks.  The âfailâ function imposes that the errors are labeled
> with polymorphic variants.  Since these are not qualified by the
> module and to allow generic treatment, a good idiom would probably be
> to use something like â`X of X.errâ where â`Xâ is sufficiently
> explicit and âX.errâ enumerates the possible failures for the module
> âXâ.  The function âcatch_exn fâ will catch exceptions raised by âfâ
> as well as failed-threads ones (by default, only turning all exceptions
> into failing thread ones).  The use of âunitâ in â('a, unit) tâ ensures
> that the thread is of type â(_, 'b) tâ â i.e. that all errors have
> been dealt with â so it can be unified with a non-polymorphic variant
> type.

The problem we have in Mirage is that our types are abstract. For
example, if you have a functor

  module FS (B : BLOCK)  = ...

then the author of the FS functor doesn't know exactly what errors the
concrete B might throw. For example, if B is a remote disk then it
might throw network errors. So ensuring that callers enumerate all
errors isn't very helpful in this case.

You can give FS an error code of [`Block_error of B.error] and just
propagate it to the caller, which is what we do now. That seems quite
clever, but I'm struggling to imagine a case where it's actually

It's probably worth compiling a list of examples where Mirage code
currently matches on specific returned Error codes and takes specific
actions. My guess is there aren't enough to justify the complexity

I tried grepping my source directory (containing git clones of various
Mirage components) with zsh:

for x in *(/); (echo '=========' $x; cd $x && git grep '| `Error (')

but in every one of these cases where we match on an `Error, we just
propagate it (i.e. the default behaviour of an exception would have
been correct).

Here are some examples:

| `Error (`Invalid_console x) -> fail (Failure (Printf.sprintf
"Invalid_console %s" x))
| `Error (`Block_device e) -> fail (Failure (Fs.string_of_block_error e))
| `Error (KV.Unknown_key key) -> fail (Invalid_argument key)
| `Error (`Unknown msg) -> Lwt.fail (Failure msg)

The only case where we do something interesting is `Error (Unknown_key
_), where we note that it was a read:

| `Error (TMPL.Unknown_key _) -> fail (Failure ("read " ^ name))

However, Unknown_key wouldn't be an exception under any proposed scheme anyway.

> This is similar to declaring
>     type ('a, 'b) t = ('a, 'b) Result.t Lwt.t
> except that one cannot ditch the âResult.tâ (and one avoids 1
> indirection per bind).
> Unfortunately, it is some work to ensure that the compiler understands
> that all errors were dealt with.  Consider
>     let m1 = fail(`X 1) >>= fun x -> fail `Y
> Then
>     let m2 = catch m1 (function `X x -> return x
>                               | e -> fail e)
> will not do, you have to write
>     let m2 = catch m1 (function `X x -> return x
>                               | `Y as e -> fail e)
> to make sure âm2â shows that â`Xâ is no longer an issue.  Thus
> defining good types to use â#typeâ is important.
> My 0.02â,
> C.

Dr Thomas Leonard        http://0install.net/
GPG: 9242 9807 C985 3C07 44A6  8B9A AE07 8280 59A5 3CC1
GPG: DA98 25AE CAD0 8975 7CDA  BD8E 0713 3F96 CA74 D8BA

MirageOS-devel mailing list



Lists.xenproject.org is hosted with RackSpace, monitoring our
servers 24x7x365 and backed by RackSpace's Fanatical Support®.