A --sanitize flag, and the attribute without which it measures nothing
ASan is an LLVM pass but instruments only functions carrying sanitize_address, which clang's C frontend adds and nothing adds to IR written by hand. Passing -fsanitize=address to the clang run over the .ll therefore instruments flan_rt.c and not one instruction of Flan: an out-of-bounds read of a defvar array, built --no-bounds-checks, printed its garbage and exited 0. With Emit naming an attribute group on every define, the same program reports global-buffer-overflow in flan.main. UBSan has no such lever. Its checks are branches the C frontend emits to __ubsan_handle_*, not a pass, so -fsanitize=undefined covers the runtime and nothing else; (<< 1 32) still goes unremarked. Recorded where it will be read rather than discovered again. The flag does not force -O0 the way --debug does -- the UB worth finding is what the optimiser does with it -- and it does pull in -g, since a report with no line costs more than the build. compile_c's cache key now digests the same cflags list the command line uses, because an unsanitized flan_rt.o served out of the cache links fine and reports nothing.
This commit is contained in:
parent
3e3d3b28f0
commit
ac5c7e9c2b
23
bin/main.ml
23
bin/main.ml
@ -72,7 +72,14 @@ let dev_flag = "--dev"
|
|||||||
stop it and read it". See [Build.opts]. *)
|
stop it and read it". See [Build.opts]. *)
|
||||||
let debug_flag = "--debug"
|
let debug_flag = "--debug"
|
||||||
|
|
||||||
let flags = [ no_checks_flag; dev_flag; debug_flag ]
|
(* ASan and UBSan over the whole program, the runtime's C and the Flan alike.
|
||||||
|
Its own flag for the same reason --debug is: it answers "is this program
|
||||||
|
touching memory it does not own", which is neither of the other two
|
||||||
|
questions. It does not imply -O0 — see [Build.opts], which also records
|
||||||
|
what each of the two sanitizers actually reaches. *)
|
||||||
|
let sanitize_flag = "--sanitize"
|
||||||
|
|
||||||
|
let flags = [ no_checks_flag; dev_flag; debug_flag; sanitize_flag ]
|
||||||
|
|
||||||
(* [--target=wasm32-wasi], the one cross target. Unlike the flags above it
|
(* [--target=wasm32-wasi], the one cross target. Unlike the flags above it
|
||||||
carries a value, so it is matched by prefix and stripped from the residual
|
carries a value, so it is matched by prefix and stripped from the residual
|
||||||
@ -153,6 +160,10 @@ let () =
|
|||||||
let checks = not (List.mem no_checks_flag args) in
|
let checks = not (List.mem no_checks_flag args) in
|
||||||
let dev = List.mem dev_flag args in
|
let dev = List.mem dev_flag args in
|
||||||
let debug = List.mem debug_flag args in
|
let debug = List.mem debug_flag args in
|
||||||
|
(* --sanitize changes the IR — every [define] names the attribute group
|
||||||
|
ASan's pass selects on — so [emit] has to honour it or what this prints
|
||||||
|
is not what a sanitized build compiles. *)
|
||||||
|
let sanitize = List.mem sanitize_flag args in
|
||||||
let files = List.filter (fun a -> not (is_flag a)) args in
|
let files = List.filter (fun a -> not (is_flag a)) args in
|
||||||
List.iter
|
List.iter
|
||||||
(fun path ->
|
(fun path ->
|
||||||
@ -160,13 +171,14 @@ let () =
|
|||||||
let l = load path in
|
let l = load path in
|
||||||
let pnames = if debug then param_names l else [] in
|
let pnames = if debug then param_names l else [] in
|
||||||
Flan.Check.program l.decls
|
Flan.Check.program l.decls
|
||||||
|> Flan.Emit.program ~checks ~dev ~debug ~pnames
|
|> Flan.Emit.program ~checks ~dev ~debug ~pnames ~sanitize
|
||||||
|> print_string))
|
|> print_string))
|
||||||
files
|
files
|
||||||
| _ :: "build" :: path :: rest ->
|
| _ :: "build" :: path :: rest ->
|
||||||
let checks = not (List.mem no_checks_flag rest) in
|
let checks = not (List.mem no_checks_flag rest) in
|
||||||
let dev = List.mem dev_flag rest in
|
let dev = List.mem dev_flag rest in
|
||||||
let debug = List.mem debug_flag rest in
|
let debug = List.mem debug_flag rest in
|
||||||
|
let sanitize = List.mem sanitize_flag rest in
|
||||||
let target = target_of rest in
|
let target = target_of rest in
|
||||||
let out =
|
let out =
|
||||||
match List.filter (fun a -> not (is_flag a)) rest with
|
match List.filter (fun a -> not (is_flag a)) rest with
|
||||||
@ -181,7 +193,7 @@ let () =
|
|||||||
| _ ->
|
| _ ->
|
||||||
prerr_endline
|
prerr_endline
|
||||||
"usage: flan build <file.flan> [-o out] [--no-bounds-checks] \
|
"usage: flan build <file.flan> [-o out] [--no-bounds-checks] \
|
||||||
[--dev] [--debug] [--target=wasm32-wasi]";
|
[--dev] [--debug] [--sanitize] [--target=wasm32-wasi]";
|
||||||
exit 2
|
exit 2
|
||||||
in
|
in
|
||||||
with_errors path (fun () ->
|
with_errors path (fun () ->
|
||||||
@ -193,7 +205,8 @@ let () =
|
|||||||
raylib and still be buildable for wasm32. *)
|
raylib and still be buildable for wasm32. *)
|
||||||
let p, csrcs, lflags = Flan.Reach.link ~dev l p in
|
let p, csrcs, lflags = Flan.Reach.link ~dev l p in
|
||||||
ignore (Flan.Build.executable
|
ignore (Flan.Build.executable
|
||||||
~opts:{ Flan.Build.default with checks; dev; debug; target }
|
~opts:{ Flan.Build.default with checks; dev; debug; sanitize;
|
||||||
|
target }
|
||||||
~csrcs ~lflags ~pnames:(if debug then param_names l else [])
|
~csrcs ~lflags ~pnames:(if debug then param_names l else [])
|
||||||
p ~out))
|
p ~out))
|
||||||
(* The daemon an editor talks to: one session, the program it belongs to
|
(* The daemon an editor talks to: one session, the program it belongs to
|
||||||
@ -272,7 +285,7 @@ let () =
|
|||||||
prerr_endline
|
prerr_endline
|
||||||
"usage: flan (read|parse|check|emit|shim) <file.flan>...\n\
|
"usage: flan (read|parse|check|emit|shim) <file.flan>...\n\
|
||||||
\ flan build <file.flan> [-o out] [--no-bounds-checks] [--dev] \
|
\ flan build <file.flan> [-o out] [--no-bounds-checks] [--dev] \
|
||||||
[--debug] [--target=wasm32-wasi]\n\
|
[--debug] [--sanitize] [--target=wasm32-wasi]\n\
|
||||||
\ flan run <file.flan> [args...]\n\
|
\ flan run <file.flan> [args...]\n\
|
||||||
\ flan reload <program.flan> <forms.flan> [-o out.so]\n\
|
\ flan reload <program.flan> <forms.flan> [-o out.so]\n\
|
||||||
\ flan dev <program.flan> [-s socket]";
|
\ flan dev <program.flan> [-s socket]";
|
||||||
|
|||||||
66
lib/build.ml
66
lib/build.ml
@ -72,6 +72,28 @@ type opts = {
|
|||||||
mechanism is a [llvm.dbg.declare] on an alloca and mem2reg deletes the
|
mechanism is a [llvm.dbg.declare] on an alloca and mem2reg deletes the
|
||||||
alloca. *)
|
alloca. *)
|
||||||
debug : bool;
|
debug : bool;
|
||||||
|
(* AddressSanitizer and UndefinedBehaviorSanitizer over the whole program:
|
||||||
|
the runtime's C, the generated shim, and — via [Emit]'s
|
||||||
|
[sanitize_address] attribute — the Flan code itself.
|
||||||
|
|
||||||
|
Its own axis and not a mode of [debug]. It deliberately does *not* force
|
||||||
|
-O0: the UB worth finding (a shift past the width folding to nothing, a
|
||||||
|
float cast that only traps once it is a real cvttss2si) is what the
|
||||||
|
optimiser does with it, so the sweep is worth running at -O2 and at -O0
|
||||||
|
and any divergence between the two is itself the finding. It does pull in
|
||||||
|
-g, because a report without a line number costs more to read than the
|
||||||
|
build costs to make.
|
||||||
|
|
||||||
|
What reaches what, measured rather than assumed:
|
||||||
|
- ASan instruments Flan functions only because [Emit] attributes them;
|
||||||
|
globals get their redzone from the module pass either way.
|
||||||
|
- UBSan instruments the C only. Its checks come out of clang's C
|
||||||
|
frontend, and there is no attribute that asks a pass for them, so
|
||||||
|
hand-written IR gets none. See [Emit]'s [sanitize] comment.
|
||||||
|
- signed-integer-overflow is excluded because wrapping is what this
|
||||||
|
language's arithmetic means; without the exclusion every program
|
||||||
|
trips on its first [+]. Nothing else is excluded. *)
|
||||||
|
sanitize : bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
(* Checks are deliberately independent of [opt]: the acceptance table runs the
|
(* Checks are deliberately independent of [opt]: the acceptance table runs the
|
||||||
@ -80,7 +102,24 @@ type opts = {
|
|||||||
checks. Dropping them is a release decision, not an optimisation one. *)
|
checks. Dropping them is a release decision, not an optimisation one. *)
|
||||||
let default =
|
let default =
|
||||||
{ target = None; opt = "-O2"; keep = false; checks = true; dev = false;
|
{ target = None; opt = "-O2"; keep = false; checks = true; dev = false;
|
||||||
debug = false }
|
debug = false; sanitize = false }
|
||||||
|
|
||||||
|
(* The flags that are neither [opt] nor the target, spelled once so that the
|
||||||
|
compile command and the object-cache key cannot disagree. They did before:
|
||||||
|
-g was written out at the command and again at the key, and a flag that
|
||||||
|
appears in one and not the other is the silent failure — an unsanitized
|
||||||
|
[flan_rt.o] served out of the cache to a sanitized build links fine and
|
||||||
|
reports nothing. *)
|
||||||
|
let cflags opts =
|
||||||
|
(if opts.debug then [ "-g" ] else [])
|
||||||
|
@ (if opts.sanitize then
|
||||||
|
(* -g here and not via [debug]: a sanitizer report with no file and no
|
||||||
|
line is most of the work still to do. *)
|
||||||
|
[ "-fsanitize=address,undefined";
|
||||||
|
"-fno-sanitize=signed-integer-overflow";
|
||||||
|
"-fno-omit-frame-pointer" ]
|
||||||
|
@ (if opts.debug then [] else [ "-g" ])
|
||||||
|
else [])
|
||||||
|
|
||||||
(* ── wasm32, which needs more than a triple ──────────────────────────
|
(* ── wasm32, which needs more than a triple ──────────────────────────
|
||||||
The native target is whatever clang was built for, so [--target=] alone is
|
The native target is whatever clang was built for, so [--target=] alone is
|
||||||
@ -278,7 +317,7 @@ let compile_c ~opts ?tflags ~src ~name () =
|
|||||||
(Digest.string
|
(Digest.string
|
||||||
(String.concat "\000"
|
(String.concat "\000"
|
||||||
[ name; src; Lazy.force clang_stamp; opts.opt;
|
[ name; src; Lazy.force clang_stamp; opts.opt;
|
||||||
(if opts.debug then "-g" else "");
|
String.concat " " (cflags opts);
|
||||||
String.concat " " tflags ]))
|
String.concat " " tflags ]))
|
||||||
in
|
in
|
||||||
let obj = Filename.concat (cachedir ()) (key ^ ".o") in
|
let obj = Filename.concat (cachedir ()) (key ^ ".o") in
|
||||||
@ -292,7 +331,7 @@ let compile_c ~opts ?tflags ~src ~name () =
|
|||||||
let cmd =
|
let cmd =
|
||||||
String.concat " "
|
String.concat " "
|
||||||
([ Filename.quote clang; opts.opt ]
|
([ Filename.quote clang; opts.opt ]
|
||||||
@ (if opts.debug then [ "-g" ] else [])
|
@ cflags opts
|
||||||
@ [ "-c" ] @ tflags
|
@ [ "-c" ] @ tflags
|
||||||
@ [ Filename.quote c; "-o"; Filename.quote tmp ])
|
@ [ Filename.quote c; "-o"; Filename.quote tmp ])
|
||||||
in
|
in
|
||||||
@ -327,14 +366,23 @@ let executable ?(opts = default) ?(csrcs = []) ?(lflags = []) ?(pnames = [])
|
|||||||
"wasm32: --debug is native only — the DWARF member offsets are computed \
|
"wasm32: --debug is native only — the DWARF member offsets are computed \
|
||||||
for the host's layout, and wasm32's 32-bit pointer moves every one of \
|
for the host's layout, and wasm32's 32-bit pointer moves every one of \
|
||||||
them";
|
them";
|
||||||
|
(* There is no wasm32 sanitizer runtime to link against: clang accepts
|
||||||
|
-fsanitize=address for the triple and the link fails on
|
||||||
|
__asan_report_load4. Refused by name rather than met at the linker. *)
|
||||||
|
if wasm_target opts && opts.sanitize then
|
||||||
|
failwith
|
||||||
|
"wasm32: --sanitize is native only — there is no libclang_rt.asan for \
|
||||||
|
wasm32-wasi to link against";
|
||||||
(* -O0 is not a choice a debug build offers: [llvm.dbg.declare] describes an
|
(* -O0 is not a choice a debug build offers: [llvm.dbg.declare] describes an
|
||||||
alloca, and at -O2 mem2reg deletes the alloca. *)
|
alloca, and at -O2 mem2reg deletes the alloca. [sanitize] deliberately
|
||||||
|
does not do this: see [opts]. *)
|
||||||
let opts = if opts.debug then { opts with opt = "-O0" } else opts in
|
let opts = if opts.debug then { opts with opt = "-O0" } else opts in
|
||||||
let tflags = target_flags opts in
|
let tflags = target_flags opts in
|
||||||
let dir = workdir () in
|
let dir = workdir () in
|
||||||
let ll = Filename.concat dir (Filename.basename out ^ ".ll") in
|
let ll = Filename.concat dir (Filename.basename out ^ ".ll") in
|
||||||
write ll
|
write ll
|
||||||
(Emit.program ~checks:opts.checks ~dev:opts.dev ~debug:opts.debug ~pnames p);
|
(Emit.program ~checks:opts.checks ~dev:opts.dev ~debug:opts.debug ~pnames
|
||||||
|
~sanitize:opts.sanitize p);
|
||||||
(* [flan_dev.c] is compiled into every build, not only a dev one. Nothing in
|
(* [flan_dev.c] is compiled into every build, not only a dev one. Nothing in
|
||||||
a release build calls into it — the compiler only emits a registry lookup
|
a release build calls into it — the compiler only emits a registry lookup
|
||||||
for a name the host was not built with, which cannot arise without cells —
|
for a name the host was not built with, which cannot arise without cells —
|
||||||
@ -368,8 +416,12 @@ let executable ?(opts = default) ?(csrcs = []) ?(lflags = []) ?(pnames = [])
|
|||||||
String.concat " "
|
String.concat " "
|
||||||
([ Filename.quote clang; opts.opt; "-Wno-override-module" ]
|
([ Filename.quote clang; opts.opt; "-Wno-override-module" ]
|
||||||
(* -g at the link so clang does not strip, and keeps the object files'
|
(* -g at the link so clang does not strip, and keeps the object files'
|
||||||
debug sections; the .ll carries its own. *)
|
debug sections; the .ll carries its own. The sanitizer flags have to
|
||||||
@ (if opts.debug then [ "-g" ] else [])
|
be here too — they are what pulls in libclang_rt.asan and the UBSan
|
||||||
|
runtime, and they are also what makes clang run the ASan pass over
|
||||||
|
the .ll, which is the only place the Flan half of the program gets
|
||||||
|
instrumented at all. *)
|
||||||
|
@ cflags opts
|
||||||
@ (if opts.dev then [ "-rdynamic" ] else [])
|
@ (if opts.dev then [ "-rdynamic" ] else [])
|
||||||
@ tflags
|
@ tflags
|
||||||
@ [ Filename.quote ll ]
|
@ [ Filename.quote ll ]
|
||||||
|
|||||||
35
lib/emit.ml
35
lib/emit.ml
@ -193,9 +193,26 @@ type m = {
|
|||||||
down because every emitter that can produce an instruction has to be able
|
down because every emitter that can produce an instruction has to be able
|
||||||
to hang a location on it. *)
|
to hang a location on it. *)
|
||||||
dbg : dbg option;
|
dbg : dbg option;
|
||||||
|
(* True in a sanitized build, and the whole of what ASan needs from us.
|
||||||
|
AddressSanitizer is an LLVM *pass*, but it instruments only functions
|
||||||
|
carrying the [sanitize_address] attribute — which clang's C frontend adds
|
||||||
|
and nothing adds to IR written by hand. Passing -fsanitize=address to the
|
||||||
|
clang run over this .ll therefore instruments the runtime's C and not one
|
||||||
|
instruction of Flan; measured, not assumed (see NEXT.md). So every
|
||||||
|
[define] here names attribute group #0 and [finish] writes it out.
|
||||||
|
|
||||||
|
There is no equivalent for UndefinedBehaviorSanitizer: its checks are
|
||||||
|
emitted by the C frontend as branches to __ubsan_handle_*, and no
|
||||||
|
attribute asks a pass to produce them. UBSan over this .ll covers the C
|
||||||
|
and nothing else. *)
|
||||||
|
sanitize : bool;
|
||||||
mutable nstr : int;
|
mutable nstr : int;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(* The attribute group every emitted function names, empty unless sanitizing.
|
||||||
|
Spelled once so the [define] sites and [finish] cannot disagree. *)
|
||||||
|
let attrs m = if m.sanitize then " #0" else ""
|
||||||
|
|
||||||
let field_ty m sn i =
|
let field_ty m sn i =
|
||||||
let s = Hashtbl.find m.structs sn in
|
let s = Hashtbl.find m.structs sn in
|
||||||
(List.nth s.Tast.fields i).Tast.fty
|
(List.nth s.Tast.fields i).Tast.fty
|
||||||
@ -1472,8 +1489,8 @@ let emit_fn m ?(hidden = false) ?(pnames = []) (fn : Tast.fn) =
|
|||||||
end
|
end
|
||||||
end;
|
end;
|
||||||
Buffer.add_string m.out
|
Buffer.add_string m.out
|
||||||
(Printf.sprintf "\ndefine %s%s%s {\nentry:\n%s%s}\n"
|
(Printf.sprintf "\ndefine %s%s%s%s {\nentry:\n%s%s}\n"
|
||||||
(if hidden then "hidden " else "") (signature ~named:true fn)
|
(if hidden then "hidden " else "") (signature ~named:true fn) (attrs m)
|
||||||
(match dsub with None -> "" | Some n -> Printf.sprintf " !dbg !%d" n)
|
(match dsub with None -> "" | Some n -> Printf.sprintf " !dbg !%d" n)
|
||||||
(Buffer.contents f.allocas) (Buffer.contents f.b))
|
(Buffer.contents f.allocas) (Buffer.contents f.b))
|
||||||
|
|
||||||
@ -1557,7 +1574,9 @@ declare void @flan_slice_fail(ptr, i64, i64, i64, i64) noreturn cold
|
|||||||
the i32 status are each optional (plan.org, Milestone-2 primitives). *)
|
the i32 status are each optional (plan.org, Milestone-2 primitives). *)
|
||||||
let emit_main m (fn : Tast.fn) =
|
let emit_main m (fn : Tast.fn) =
|
||||||
let b = Buffer.create 256 in
|
let b = Buffer.create 256 in
|
||||||
Buffer.add_string b "\ndefine i32 @main(i32 %argc, ptr %argv) {\nentry:\n";
|
Buffer.add_string b
|
||||||
|
(Printf.sprintf "\ndefine i32 @main(i32 %%argc, ptr %%argv)%s {\nentry:\n"
|
||||||
|
(attrs m));
|
||||||
Buffer.add_string b " call void @flan_rt_init(i32 %argc, ptr %argv)\n";
|
Buffer.add_string b " call void @flan_rt_init(i32 %argc, ptr %argv)\n";
|
||||||
(* The program's own end of the transfer channel. Nothing can be transferring
|
(* The program's own end of the transfer channel. Nothing can be transferring
|
||||||
when [main] returns: a restart is found by name on the restart stack, and
|
when [main] returns: a restart is found by name on the restart stack, and
|
||||||
@ -1616,12 +1635,13 @@ let new_dbg (p : Tast.program) =
|
|||||||
file);
|
file);
|
||||||
d
|
d
|
||||||
|
|
||||||
let new_module ~checks ~dev ~known ?(debug = false) (p : Tast.program) =
|
let new_module ~checks ~dev ~known ?(debug = false) ?(sanitize = false)
|
||||||
|
(p : Tast.program) =
|
||||||
let m = {
|
let m = {
|
||||||
out = Buffer.create 8192; strs = Buffer.create 512;
|
out = Buffer.create 8192; strs = Buffer.create 512;
|
||||||
structs = Hashtbl.create 16; globals = Hashtbl.create 16;
|
structs = Hashtbl.create 16; globals = Hashtbl.create 16;
|
||||||
externs = Hashtbl.create 32;
|
externs = Hashtbl.create 32;
|
||||||
checks; dev; known; nstr = 0;
|
checks; dev; known; nstr = 0; sanitize;
|
||||||
dbg = (if debug then Some (new_dbg p) else None);
|
dbg = (if debug then Some (new_dbg p) else None);
|
||||||
} in
|
} in
|
||||||
List.iter (fun (s : Tast.structure) -> Hashtbl.replace m.structs s.Tast.sname s)
|
List.iter (fun (s : Tast.structure) -> Hashtbl.replace m.structs s.Tast.sname s)
|
||||||
@ -1676,13 +1696,14 @@ let dmodule d =
|
|||||||
|
|
||||||
let finish m =
|
let finish m =
|
||||||
header ^ Buffer.contents m.strs ^ "\n" ^ Buffer.contents m.out
|
header ^ Buffer.contents m.strs ^ "\n" ^ Buffer.contents m.out
|
||||||
|
^ (if m.sanitize then "\nattributes #0 = { sanitize_address }\n" else "")
|
||||||
^ (match m.dbg with None -> "" | Some d -> dmodule d)
|
^ (match m.dbg with None -> "" | Some d -> dmodule d)
|
||||||
|
|
||||||
(* [checks] is on by default: a dev build traps on an out-of-bounds [at] or
|
(* [checks] is on by default: a dev build traps on an out-of-bounds [at] or
|
||||||
[slice], a release build is told to drop them. *)
|
[slice], a release build is told to drop them. *)
|
||||||
let program ?(checks = true) ?(dev = false) ?(debug = false) ?(pnames = [])
|
let program ?(checks = true) ?(dev = false) ?(debug = false) ?(pnames = [])
|
||||||
(p : Tast.program) : string =
|
?(sanitize = false) (p : Tast.program) : string =
|
||||||
let m = new_module ~checks ~dev ~known:(fun _ -> true) ~debug p in
|
let m = new_module ~checks ~dev ~known:(fun _ -> true) ~debug ~sanitize p in
|
||||||
(* One cell per function, initialised to the function this build compiled.
|
(* One cell per function, initialised to the function this build compiled.
|
||||||
Nothing has been redefined yet, so a dev build starts out behaving exactly
|
Nothing has been redefined yet, so a dev build starts out behaving exactly
|
||||||
like a release one — the indirection is the only difference. *)
|
like a release one — the indirection is the only difference. *)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user