diff --git a/Changes b/Changes index 29d413ce4..f6ba5c411 100644 --- a/Changes +++ b/Changes @@ -140,6 +140,13 @@ Working version (KC Sivaramakrishnan, reported by Enguerrand Decorne, review by Gabriel Scherer, and Xavier Leroy) +- #9534, #9947: Introduce a naked pointers checker mode to the runtime + (configure option --enable-naked-pointers-checker). Alarms are printed + when the garbage collector finds out-of-heap pointers that could + cause a crash in no-naked-pointers mode. + (Enguerrand Decorne, KC Sivaramakrishnan, Xavier Leroy, Stephen Dolan, + David Allsopp, Nicolás Ojeda Bär review by Xavier Leroy, Nicolás Ojeda Bär) + ### Code generation and optimizations: - #9551: ocamlc no longer loads DLLs at link time to check that diff --git a/configure b/configure index 2209a2ea1..bafcde3f9 100755 --- a/configure +++ b/configure @@ -747,6 +747,7 @@ build_os build_vendor build_cpu build +naked_pointers_checker naked_pointers compute_deps stdlib_manpages @@ -897,6 +898,7 @@ enable_ocamldoc enable_ocamltest enable_frame_pointers enable_naked_pointers +enable_naked_pointers_checker enable_spacetime enable_call_counts enable_cfi @@ -1570,6 +1572,8 @@ Optional Features: --enable-frame-pointers use frame pointers in runtime and generated code --disable-naked-pointers do not allow naked pointers + --enable-naked-pointers-checker + enable the naked pointers checker --enable-spacetime build the spacetime profiler --disable-call-counts disable the call counts in spacetime --disable-cfi disable the CFI directives in assembly files @@ -2941,6 +2945,7 @@ VERSION=4.12.0+dev0-2020-04-22 + ## Generated files @@ -3201,6 +3206,12 @@ if test "${enable_naked_pointers+set}" = set; then : fi +# Check whether --enable-naked-pointers-checker was given. +if test "${enable_naked_pointers_checker+set}" = set; then : + enableval=$enable_naked_pointers_checker; +fi + + # Check whether --enable-spacetime was given. if test "${enable_spacetime+set}" = set; then : enableval=$enable_spacetime; @@ -16653,6 +16664,27 @@ else naked_pointers=true fi +if test x"$enable_naked_pointers_checker" = "xyes" ; then : + if test x"$enable_naked_pointers" = "xno" ; then : + as_fn_error $? "--enable-naked-pointers-checker and --disable-naked-pointers are incompatible" "$LINENO" 5 +fi + case "$arch","$system" in #( + amd64,linux|amd64,macosx \ + |amd64,openbsd|amd64,win64 \ + |amd64,freebsd) : + naked_pointers_checker=true + $as_echo "#define NAKED_POINTERS_CHECKER 1" >>confdefs.h + ;; #( + *) : + as_fn_error $? "naked pointers checker not supported on this platform" "$LINENO" 5 + ;; #( + *) : + ;; +esac +else + naked_pointers_checker=false +fi + ## Check for mmap support for huge pages and contiguous heap { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether mmap supports huge pages" >&5 diff --git a/configure.ac b/configure.ac index cddb970f7..07f99740f 100644 --- a/configure.ac +++ b/configure.ac @@ -170,6 +170,7 @@ AC_SUBST([PACKLD]) AC_SUBST([stdlib_manpages]) AC_SUBST([compute_deps]) AC_SUBST([naked_pointers]) +AC_SUBST([naked_pointers_checker]) ## Generated files @@ -287,6 +288,10 @@ AC_ARG_ENABLE([naked-pointers], [AS_HELP_STRING([--disable-naked-pointers], [do not allow naked pointers])]) +AC_ARG_ENABLE([naked-pointers-checker], + [AS_HELP_STRING([--enable-naked-pointers-checker], + [enable the naked pointers checker])]) + AC_ARG_ENABLE([spacetime], [AS_HELP_STRING([--enable-spacetime], [build the spacetime profiler])]) @@ -1693,6 +1698,22 @@ AS_IF([test x"$enable_naked_pointers" = "xno" ], AC_DEFINE([NO_NAKED_POINTERS])], [naked_pointers=true]) +AS_IF([test x"$enable_naked_pointers_checker" = "xyes" ], + [AS_IF([test x"$enable_naked_pointers" = "xno" ], + [AC_MSG_ERROR(m4_normalize([ + --enable-naked-pointers-checker and --disable-naked-pointers + are incompatible]))]) + AS_CASE(["$arch","$system"], + [amd64,linux|amd64,macosx \ + |amd64,openbsd|amd64,win64 \ + |amd64,freebsd], + [naked_pointers_checker=true + AC_DEFINE([NAKED_POINTERS_CHECKER])], + [*], + [AC_MSG_ERROR([naked pointers checker not supported on this platform])] + )], + [naked_pointers_checker=false]) + ## Check for mmap support for huge pages and contiguous heap OCAML_MMAP_SUPPORTS_HUGE_PAGES diff --git a/runtime/caml/domain_state.tbl b/runtime/caml/domain_state.tbl index da49d3454..f094d37f7 100644 --- a/runtime/caml/domain_state.tbl +++ b/runtime/caml/domain_state.tbl @@ -84,3 +84,8 @@ DOMAIN_STATE(uintnat, eventlog_paused) DOMAIN_STATE(uintnat, eventlog_enabled) DOMAIN_STATE(FILE*, eventlog_out) /* See eventlog.c */ + +#if defined(NAKED_POINTERS_CHECKER) && !defined(_WIN32) +DOMAIN_STATE(void*, checking_pointer_pc) +/* See major_gc.c */ +#endif diff --git a/runtime/caml/m.h.in b/runtime/caml/m.h.in index 0e099134c..e12946fb8 100644 --- a/runtime/caml/m.h.in +++ b/runtime/caml/m.h.in @@ -87,6 +87,8 @@ #undef NO_NAKED_POINTERS +#undef NAKED_POINTERS_CHECKER + #undef WITH_PROFINFO #undef CAML_WITH_FPIC diff --git a/runtime/domain.c b/runtime/domain.c index c6cb4cd2f..d4d8de53f 100644 --- a/runtime/domain.c +++ b/runtime/domain.c @@ -87,4 +87,8 @@ void caml_init_domain () Caml_state->eventlog_startup_pid = 0; Caml_state->eventlog_startup_timestamp = 0; Caml_state->eventlog_out = NULL; + +#if defined(NAKED_POINTERS_CHECKER) && !defined(_WIN32) + Caml_state->checking_pointer_pc = NULL; + #endif } diff --git a/runtime/major_gc.c b/runtime/major_gc.c index 27845a2b4..dde350213 100644 --- a/runtime/major_gc.c +++ b/runtime/major_gc.c @@ -218,7 +218,7 @@ Caml_inline void mark_stack_push(struct mark_stack* stk, value block, CAMLassert(Tag_val(block) != Infix_tag); CAMLassert(Tag_val(block) < No_scan_tag); -#ifdef NO_NAKED_POINTERS +#if defined(NO_NAKED_POINTERS) || defined(NAKED_POINTERS_CHECKER) if (Tag_val(block) == Closure_tag) { /* Skip the code pointers and integers at beginning of closure; start scanning at the first word of the environment part. */ @@ -272,13 +272,16 @@ Caml_inline void mark_stack_push(struct mark_stack* stk, value block, me->offset = offset; } +#if defined(NAKED_POINTERS_CHECKER) && defined(NATIVE_CODE) +static void is_naked_pointer_safe (value v, value *p); +#endif -void caml_darken (value v, value *p /* not used */) +void caml_darken (value v, value *p) { #ifdef NO_NAKED_POINTERS - if (Is_block (v) && !Is_young (v)) { + if (Is_block(v) && !Is_young (v)) { #else - if (Is_block (v) && Is_in_heap (v)) { + if (Is_block(v) && Is_in_heap (v)) { #endif header_t h = Hd_val (v); tag_t t = Tag_hd (h); @@ -302,6 +305,11 @@ void caml_darken (value v, value *p /* not used */) } } } +#if defined(NAKED_POINTERS_CHECKER) && defined(NATIVE_CODE) + else if (Is_block(v) && !Is_young(v)) { + is_naked_pointer_safe(v, p); + } +#endif } /* This function shrinks the mark stack back to the MARK_STACK_INIT_SIZE size @@ -405,7 +413,7 @@ Caml_inline void mark_slice_darken(struct mark_stack* stk, value v, mlsize_t i, child = Field (v, i); #ifdef NO_NAKED_POINTERS - if (Is_block (child) && !Is_young (child)) { + if (Is_block (child) && ! Is_young (child)) { #else if (Is_block (child) && Is_in_heap (child)) { #endif @@ -452,6 +460,11 @@ Caml_inline void mark_slice_darken(struct mark_stack* stk, value v, mlsize_t i, } } } +#if defined(NAKED_POINTERS_CHECKER) && defined(NATIVE_CODE) + else if (Is_block(child) && ! Is_young(child)) { + is_naked_pointer_safe(child, &Field (v, i)); + } +#endif } static void mark_ephe_aux (struct mark_stack *stk, intnat *work, @@ -542,8 +555,6 @@ static void mark_ephe_aux (struct mark_stack *stk, intnat *work, } } - - static void mark_slice (intnat work) { mark_entry me = {0, 0}; @@ -1077,3 +1088,91 @@ void caml_finalise_heap (void) while (caml_gc_phase == Phase_sweep) sweep_slice (LONG_MAX); } + +#if defined(NAKED_POINTERS_CHECKER) && defined(NATIVE_CODE) + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include + +Caml_inline int safe_load(volatile header_t * p, header_t * result) +{ + header_t v; + __try { + v = *p; + } + __except(GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION ? + EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { + *result = 0xdeadbeef; + return 0; + } + *result = v; + return 1; +} + +#else + +Caml_inline int safe_load (header_t * addr, /*out*/ header_t * contents) +{ + int ok; + header_t h; + intnat tmp; + + asm volatile( + "leaq 1f(%%rip), %[tmp] \n\t" + "movq %[tmp], 0(%[handler]) \n\t" + "xorl %[ok], %[ok] \n\t" + "movq 0(%[addr]), %[h] \n\t" + "movl $1, %[ok] \n\t" + "1: \n\t" + "xorq %[tmp], %[tmp] \n\t" + "movq %[tmp], 0(%[handler])" + : [tmp] "=&r" (tmp), [ok] "=&r" (ok), [h] "=&r" (h) + : [addr] "r" (addr), + [handler] "r" (&(Caml_state->checking_pointer_pc))); + *contents = h; + return ok; +} + +#endif + +static void is_naked_pointer_safe (value v, value *p) +{ + header_t h; + tag_t t; + + /* The following conditions were checked by the caller */ + CAMLassert(Is_block(v) && !Is_young(v) && !Is_in_heap(v)); + + if (! safe_load(&Hd_val(v), &h)) goto on_segfault; + + t = Tag_hd(h); + if (t == Infix_tag) { + v -= Infix_offset_hd(h); + if (! safe_load(&Hd_val(v), &h)) goto on_segfault; + t = Tag_hd(h); + } + + /* For the out-of-heap pointer to be considered safe, + * it should have a black header and its size should be < 2 ** 40 + * words (128 GB). If not, we report a warning. */ + if (Is_black_hd(h) && Wosize_hd(h) < (INT64_LITERAL(1) << 40)) + return; + + if (!Is_black_hd(h)) { + fprintf (stderr, "Out-of-heap pointer at %p of value %p has " + "non-black head (tag=%d)\n", p, (void*)v, t); + } else { + fprintf (stderr, + "Out-of-heap pointer at %p of value %p has " + "suspiciously large size: %" ARCH_INT64_PRINTF_FORMAT "u words\n", + p, (void*)v, Wosize_hd(h)); + } + return; + + on_segfault: + fprintf (stderr, "Out-of-heap pointer at %p of value %p. " + "Cannot read head.\n", p, (void*)v); +} + +#endif diff --git a/runtime/signals_nat.c b/runtime/signals_nat.c index 93ec04745..0e7c3ebe3 100644 --- a/runtime/signals_nat.c +++ b/runtime/signals_nat.c @@ -231,6 +231,14 @@ DECLARE_SIGNAL_HANDLER(segv_handler) #endif caml_raise_stack_overflow(); #endif +#ifdef NAKED_POINTERS_CHECKER + } else if (Caml_state->checking_pointer_pc) { +#ifdef CONTEXT_PC + CONTEXT_PC = (context_reg)Caml_state->checking_pointer_pc; +#else +#error "CONTEXT_PC must be defined if RETURN_AFTER_STACK_OVERFLOW is" +#endif /* CONTEXT_PC */ +#endif /* NAKED_POINTERS_CHECKER */ } else { /* Otherwise, deactivate our exception handler and return, causing fatal signal to be generated at point of error. */ diff --git a/runtime/sys.c b/runtime/sys.c index a131bcc17..909a75f65 100644 --- a/runtime/sys.c +++ b/runtime/sys.c @@ -698,3 +698,12 @@ CAMLprim value caml_sys_isatty(value chan) return ret; } + +CAMLprim value caml_sys_const_naked_pointers_checked(value unit) +{ +#ifdef NAKED_POINTERS_CHECKER + return Val_true; +#else + return Val_false; +#endif +} diff --git a/stdlib/stdlib.ml b/stdlib/stdlib.ml index 52debb5b8..3c942cb98 100644 --- a/stdlib/stdlib.ml +++ b/stdlib/stdlib.ml @@ -565,6 +565,11 @@ let exit retcode = let _ = register_named_value "Pervasives.do_at_exit" do_at_exit +external major : unit -> unit = "caml_gc_major" +external naked_pointers_checked : unit -> bool + = "caml_sys_const_naked_pointers_checked" +let () = if naked_pointers_checked () then at_exit major + (*MODULE_ALIASES*) module Arg = Arg module Array = Array diff --git a/testsuite/tests/runtime-naked-pointers/cstubs.c b/testsuite/tests/runtime-naked-pointers/cstubs.c new file mode 100644 index 000000000..e9315f3ae --- /dev/null +++ b/testsuite/tests/runtime-naked-pointers/cstubs.c @@ -0,0 +1,20 @@ +#include +#include "caml/mlvalues.h" +#include "caml/gc.h" +#include "caml/memory.h" + +static int colors[4] = { Caml_white, Caml_gray, Caml_blue, Caml_black }; + +value make_block(value header_size, value color, value size) +{ + intnat sz = Nativeint_val(size); + value * p = caml_stat_alloc((1 + sz) * sizeof(value)); + p[0] = Make_header(Nativeint_val(header_size), 0, colors[Int_val(color)]); + memset(p + 1, 0x80, sz * sizeof(value)); + return (value) (p + 1); +} + +value make_raw_pointer (value v) +{ + return (value) Nativeint_val(v); +} diff --git a/testsuite/tests/runtime-naked-pointers/np.ml b/testsuite/tests/runtime-naked-pointers/np.ml new file mode 100644 index 000000000..1738934ff --- /dev/null +++ b/testsuite/tests/runtime-naked-pointers/np.ml @@ -0,0 +1,11 @@ +type color = White | Gray | Blue | Black + +external make_block: nativeint -> color -> nativeint -> Obj.t + = "make_block" + +external make_raw_pointer: nativeint -> Obj.t + = "make_raw_pointer" + +let do_gc root = + Gc.compact(); (* full major + compaction *) + root diff --git a/testsuite/tests/runtime-naked-pointers/np1.ml b/testsuite/tests/runtime-naked-pointers/np1.ml new file mode 100644 index 000000000..be4c677a2 --- /dev/null +++ b/testsuite/tests/runtime-naked-pointers/np1.ml @@ -0,0 +1,12 @@ +(* TEST + modules = "cstubs.c np.ml" + * bytecode + * native +*) + +open Np + +(* Out-of-heap object with black header is accepted even in no-naked-pointers + mode. GC doesn't scan black objects. *) + +let x = do_gc [ make_block 100n Black 100n ] diff --git a/testsuite/tests/runtime-naked-pointers/np2.ml b/testsuite/tests/runtime-naked-pointers/np2.ml new file mode 100644 index 000000000..f24c813c2 --- /dev/null +++ b/testsuite/tests/runtime-naked-pointers/np2.ml @@ -0,0 +1,13 @@ +(* TEST + modules = "cstubs.c np.ml" + * bytecode + * native +*) + +open Np + +(* Out-of-heap object with black header is accepted even in no-naked-pointers + mode. GC doesn't scan black objects. However, if the size in the + head is crazily big, the naked pointer detector will warn. *) + +let x = do_gc [ make_block (-1n) Black 100n ] diff --git a/testsuite/tests/runtime-naked-pointers/np2.run b/testsuite/tests/runtime-naked-pointers/np2.run new file mode 100755 index 000000000..c03f6f688 --- /dev/null +++ b/testsuite/tests/runtime-naked-pointers/np2.run @@ -0,0 +1,3 @@ +#!/bin/sh + +exec ${test_source_directory}/runtest.sh diff --git a/testsuite/tests/runtime-naked-pointers/np3.ml b/testsuite/tests/runtime-naked-pointers/np3.ml new file mode 100644 index 000000000..d207279df --- /dev/null +++ b/testsuite/tests/runtime-naked-pointers/np3.ml @@ -0,0 +1,15 @@ +(* TEST + modules = "cstubs.c np.ml" + * naked_pointers + ** bytecode + ** native +*) + +open Np + +(* Out-of-heap object with non-black header is OK in naked pointers mode only *) +(* Note that the header size can be wrong as it should not be used by the GC *) + +let x = do_gc [ make_block 10000n White 10n; + make_block 1n Blue 0n; + make_block (-1n) Gray 5n ] diff --git a/testsuite/tests/runtime-naked-pointers/np3.run b/testsuite/tests/runtime-naked-pointers/np3.run new file mode 100755 index 000000000..c03f6f688 --- /dev/null +++ b/testsuite/tests/runtime-naked-pointers/np3.run @@ -0,0 +1,3 @@ +#!/bin/sh + +exec ${test_source_directory}/runtest.sh diff --git a/testsuite/tests/runtime-naked-pointers/np4.ml b/testsuite/tests/runtime-naked-pointers/np4.ml new file mode 100644 index 000000000..98966ddff --- /dev/null +++ b/testsuite/tests/runtime-naked-pointers/np4.ml @@ -0,0 +1,13 @@ +(* TEST + modules = "cstubs.c np.ml" + * naked_pointers + ** bytecode + ** native +*) + +open Np + +(* Null pointers and bad pointers outside the heap are OK + in naked pointers mode only *) + +let x = do_gc [ make_raw_pointer 0n; make_raw_pointer 42n ] diff --git a/testsuite/tests/runtime-naked-pointers/np4.run b/testsuite/tests/runtime-naked-pointers/np4.run new file mode 100755 index 000000000..c03f6f688 --- /dev/null +++ b/testsuite/tests/runtime-naked-pointers/np4.run @@ -0,0 +1,3 @@ +#!/bin/sh + +exec ${test_source_directory}/runtest.sh diff --git a/testsuite/tests/runtime-naked-pointers/runtest.sh b/testsuite/tests/runtime-naked-pointers/runtest.sh new file mode 100755 index 000000000..f5d4df561 --- /dev/null +++ b/testsuite/tests/runtime-naked-pointers/runtest.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +if grep -q "#define NAKED_POINTERS_CHECKER" ${ocamlsrcdir}/runtime/caml/m.h \ +&& (echo ${program} | grep -q '\.opt') +then + (${program} > ${output}) 2>&1 | grep -q '^Out-of-heap ' + exit $? +else + exec ${program} > ${output} +fi