package krb

  1. Overview
  2. Docs
Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source

Source file context_sequencer.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
open! Core
open Async


type t = Context.t Throttle.Sequencer.t

let the_t : t Lazy_deferred.t =
  Lazy_deferred.create (fun () ->
    (* Before calling down into libkrb5, we release the OCaml runtime. We do our calls in
       another thread, so it is possible for caml_sys_exit to be initiated while we are
       still doing Kerberos things. Because libkrb5 is dynamically linked, this can cause
       undefined behavior, most often segfaults or assertion failures.

       We make sure all outstanding calls (including the [Context.init] call) are
       completed before letting Async shutdown complete.

       Because [shutdown] only calls the handlers that have been registered at the time
       that [shutdown] is called, we have to be careful not to call [Context.init] if we
       are already shutting down. *)
    if Shutdown.is_shutting_down ()
    then
      failwith
        "Not initializing global Kerberos context because async is already shutting down";
    Krb_debug.log_s (fun () -> [%message "Initializing global Kerberos context"]);
    let context_initialized = Ivar.create () in
    Shutdown.don't_finish_before (Ivar.read context_initialized);
    let%map t =
      In_thread.run Context.init
      >>| (fun result ->
        Ivar.fill context_initialized ();
        result)
      >>| Result.map_error ~f:(fun code ->
        let krb_error = Krb_error.to_string ~info:"krb5_init_context" code in
        match Krb_info.sandbox_tag with
        | Some tag ->
          Error.create_s
            [%message
              "Failed to initialize global Krb context"
                ~_:(krb_error : string)
                (code : int32)
                (tag : Sexp.t)]
        | None ->
          Error.create_s
            [%message
              "Failed to initialize global Krb context"
                ~_:(krb_error : string)
                (code : int32)])
      >>| ok_exn
      >>| Throttle.Sequencer.create
    in
    Shutdown.at_shutdown (fun () ->
      Throttle.prior_jobs_done t
      );
    t)
;;

(* This is the monitor that we run in when calling [Gc.add_finalizer]. [Gc.add_finalizer]
   stores a reference to the current monitor. Because finalizers are GC roots, this
   prevents the monitor from being GC'd until the finalizer is caller. It isn't too
   difficult to get yourself into a situation where the monitor holds a reference to the
   thing you are adding a finalizer for. When this happens, the finalizer will never run
   and the monitor will never be GC'd.

   To make the above a bit more concrete, take a look at
   lib/krb/jane/test/bin/finalizer_memory_leak.ml *)
let finalizer_monitor =
  lazy
    (let monitor = Monitor.create ~name:"Krb.Context_sequencer" () in
     (* We have to detach the monitor so it doesn't hold onto a reference to it's parent
        monitor (i.e. the current monitor when this lazy is forced). We don't expect any
        of the finalizers to raise, nor do we really have anything useful to do with the
        exception, so we just ignore it. *)
     Monitor.detach_and_iter_errors monitor ~f:(ignore : exn -> unit);
     monitor)
;;

(* This will raise if [f] raises or if forcing [the_t] raises. The latter can happen if
   you are in the kerberos sandbox. *)
let enqueue_job_internal_exn ~f =
  Lazy_deferred.force_exn the_t >>= fun t -> Throttle.enqueue t (fun c -> f c)
;;

let enqueue_job_internal_krb_result ~f =
  match%bind Lazy_deferred.force the_t with
  | Error error -> Deferred.Result.fail (`Raised error)
  | Ok t ->
    (match%bind Throttle.enqueue' t (fun c -> f c) with
     | `Aborted -> assert false (* We don't call [Throttle.abort] *)
     | `Raised exn -> Deferred.Result.fail (`Raised (Error.of_exn exn))
     | `Ok (Ok res) -> return (Ok res)
     | `Ok (Error code) -> Deferred.Result.fail (`Krb_error code))
;;

let enqueue_job_exn ~f =
  enqueue_job_internal_exn ~f:(fun c -> In_thread.run (fun () -> f c))
;;

let gen_error_msg ~enqueue ~(info : _ Krb_info.t) code =
  let%bind krb_error =
    enqueue ~f:(fun context -> Krb_error.to_string ~context ~info:info.function_ code)
  in
  match%map Krb_info.tags info code with
  | None -> Error.create_s [%message "" ~_:(krb_error : string)]
  | Some tags -> Error.create_s [%message "" ~_:(krb_error : string) ~_:(tags : Sexp.t)]
;;

let debug_before_job ~(info : _ Krb_info.t) ~is_blocking () =
  Krb_debug.log_s (fun () ->
    match info.tag_arguments with
    | None ->
      [%message
        "Calling Kerberos function" ~info:(info.function_ : string) (is_blocking : bool)]
    | Some tags ->
      let tags = Lazy.force tags in
      [%message
        "Calling Kerberos function"
          ~info:(info.function_ : string)
          (is_blocking : bool)
          (tags : Sexp.t)])
;;

let debug_after_job ~(info : 'a Krb_info.t) result =
  Krb_debug.log_s (fun () ->
    let tags =
      match result, info.tag_result with
      | Ok result, Some get_tags -> Some (get_tags result)
      | Ok _, None -> None
      | Error error, _ -> Some ([%sexp_of: Error.t] error)
    in
    match tags with
    | None -> [%message "Called Kerberos function" ~info:(info.function_ : string)]
    | Some tags ->
      [%message
        "Called Kerberos function" ~info:(info.function_ : string) (tags : Sexp.t)])
;;

let enqueue_job_with_info_aux ~info ~error_msg ~is_blocking ~f =
  debug_before_job ~info ~is_blocking ();
  match%bind enqueue_job_internal_krb_result ~f with
  | Ok result ->
    debug_after_job ~info (Ok result);
    return (Ok result)
  | Error (`Raised _ as raised) -> Deferred.Result.fail raised
  | Error (`Krb_error code) ->
    let%bind error = error_msg ~info code in
    debug_after_job ~info (Error error);
    Deferred.Result.fail (`Krb_error (error, code))
;;

let error_msg_non_blocking ~info code = gen_error_msg ~enqueue:enqueue_job_exn ~info code

let enqueue_job_with_info' ~info ~f =
  enqueue_job_with_info_aux
    ~info
    ~error_msg:error_msg_non_blocking
    ~is_blocking:false
    ~f:(fun c -> In_thread.run (fun () -> f c))
;;

let enqueue_job_with_info ~info ~f =
  enqueue_job_with_info' ~info ~f
  |> Deferred.Result.map_error ~f:(function
    | `Raised error -> error
    | `Krb_error (error, _code) -> error)
;;

let add_finalizer arg ~f:finalize =
  Scheduler.within ~monitor:(force finalizer_monitor) (fun () ->
    Gc.add_finalizer_exn arg (fun arg ->
      don't_wait_for (enqueue_job_exn ~f:(fun c -> finalize c arg))))
;;

module Expert = struct
  let enqueue_job_blocking_exn ~f = enqueue_job_internal_exn ~f:(fun c -> return (f c))

  let error_msg_blocking ~info code =
    gen_error_msg ~enqueue:enqueue_job_blocking_exn ~info code
  ;;

  let enqueue_job_with_info_blocking ~info ~f =
    enqueue_job_with_info_aux
      ~info
      ~error_msg:error_msg_blocking
      ~is_blocking:true
      ~f:(fun c -> return (f c))
    |> Deferred.Result.map_error ~f:(function
      | `Raised error -> error
      | `Krb_error (error, _code) -> error)
  ;;
end

(* Payloads up to this threshold will be encrypted on the main thread,
   blocking Async. Anything above this threshold will be encrypted on a separate thread.
   This was chosen somewhat arbitrarily based on benchmark results. An
   encryption/decryption roundtrip for a 1MB payload is around 60ms. *)
let threshold_for_blocking_encryption = 1024 * 1024

let enqueue_blocking_if_below_encryption_size_threshold ~data_size =
  if data_size <= threshold_for_blocking_encryption
  then Expert.enqueue_job_with_info_blocking
  else enqueue_job_with_info
;;
OCaml

Innovation. Community. Security.