[pypy-commit] pypy py3.6-sandbox-2: merge 8bd962a47352

arigo pypy.commits at gmail.com
Mon Aug 26 11:27:49 EDT 2019


Author: Armin Rigo <arigo at tunes.org>
Branch: py3.6-sandbox-2
Changeset: r97272:3d2a0a3328e7
Date: 2019-08-26 17:20 +0200
http://bitbucket.org/pypy/pypy/changeset/3d2a0a3328e7/

Log:	merge 8bd962a47352

diff --git a/pypy/module/__pypy__/interp_debug.py b/pypy/module/__pypy__/interp_debug.py
--- a/pypy/module/__pypy__/interp_debug.py
+++ b/pypy/module/__pypy__/interp_debug.py
@@ -1,7 +1,13 @@
 from pypy.interpreter.gateway import unwrap_spec
 from rpython.rlib import debug, jit
 from rpython.rlib import rtimer
+from rpython.rlib.objectmodel import sandbox_review
 
+# In sandbox mode, the debug_start/debug_print functions are disabled,
+# because they could allow the attacker to write arbitrary bytes to stderr
+
+
+ at sandbox_review(abort=True)
 @jit.dont_look_inside
 @unwrap_spec(category='text', timestamp=bool)
 def debug_start(space, category, timestamp=False):
@@ -10,11 +16,13 @@
         return space.newint(res)
     return space.w_None
 
+ at sandbox_review(abort=True)
 @jit.dont_look_inside
 def debug_print(space, args_w):
     parts = [space.text_w(space.str(w_item)) for w_item in args_w]
     debug.debug_print(' '.join(parts))
 
+ at sandbox_review(abort=True)
 @jit.dont_look_inside
 @unwrap_spec(category='text', timestamp=bool)
 def debug_stop(space, category, timestamp=False):
@@ -23,6 +31,7 @@
         return space.newint(res)
     return space.w_None
 
+ at sandbox_review(abort=True)
 @unwrap_spec(category='text')
 def debug_print_once(space, category, args_w):
     debug_start(space, category)
@@ -34,9 +43,16 @@
 def debug_flush(space):
     debug.debug_flush()
 
+
+# In sandbox mode, these two helpers are disabled because they give unlimited
+# access to the real time (if you enable them, note that they use lloperations
+# that must also be white-listed in graphchecker.py)
+
+ at sandbox_review(abort=True)
 def debug_read_timestamp(space):
     return space.newint(rtimer.read_timestamp())
 
+ at sandbox_review(abort=True)
 def debug_get_timestamp_unit(space):
     unit = rtimer.get_timestamp_unit()
     if unit == rtimer.UNIT_TSC:
diff --git a/pypy/module/array/interp_array.py b/pypy/module/array/interp_array.py
--- a/pypy/module/array/interp_array.py
+++ b/pypy/module/array/interp_array.py
@@ -1,6 +1,6 @@
 from rpython.rlib import jit, rgc, rutf8
 from rpython.rlib.buffer import RawBuffer, SubBuffer
-from rpython.rlib.objectmodel import keepalive_until_here
+from rpython.rlib.objectmodel import keepalive_until_here, sandbox_review
 from rpython.rlib.rarithmetic import ovfcheck, widen, r_uint
 from rpython.rlib.unroll import unrolling_iterable
 from rpython.rtyper.annlowlevel import llstr
@@ -179,6 +179,7 @@
         if self._buffer:
             lltype.free(self._buffer, flavor='raw')
 
+    @sandbox_review(reviewed=True)
     def setlen(self, size, zero=False, overallocate=True):
         if self._buffer:
             delta_memory_pressure = -self.allocated * self.itemsize
@@ -253,6 +254,7 @@
     def _charbuf_stop(self):
         keepalive_until_here(self)
 
+    @sandbox_review(reviewed=True)
     def delitem(self, space, i, j):
         if i < 0:
             i += self.len
@@ -407,6 +409,7 @@
         self._charbuf_stop()
         return self.space.newbytes(s)
 
+    @sandbox_review(reviewed=True)
     def descr_fromstring(self, space, w_s):
         """ fromstring(string)
 
@@ -565,6 +568,7 @@
                                 w_bytes]),
                 w_dict])
 
+    @sandbox_review(reviewed=True)
     def descr_copy(self, space):
         """ copy(array)
 
@@ -579,6 +583,7 @@
         )
         return w_a
 
+    @sandbox_review(reviewed=True)
     def descr_byteswap(self, space):
         """ byteswap()
 
@@ -659,6 +664,7 @@
     def descr_iter(self, space):
         return space.newseqiter(self)
 
+    @sandbox_review(reviewed=True)
     def descr_add(self, space, w_other):
         if (not isinstance(w_other, W_ArrayBase)
                 or w_other.typecode != self.typecode):
@@ -682,6 +688,7 @@
         keepalive_until_here(a)
         return a
 
+    @sandbox_review(reviewed=True)
     def descr_inplace_add(self, space, w_other):
         if (not isinstance(w_other, W_ArrayBase)
                 or w_other.typecode != self.typecode):
@@ -700,6 +707,7 @@
         keepalive_until_here(w_other)
         return self
 
+    @sandbox_review(reviewed=True)
     def _mul_helper(self, space, w_repeat, is_inplace):
         try:
             repeat = space.getindex_w(w_repeat, space.w_OverflowError)
@@ -1071,6 +1079,7 @@
                                          self.space.newtext(msg))
             return result
 
+        @sandbox_review(reviewed=True)
         def fromsequence(self, w_seq):
             space = self.space
             oldlen = self.len
@@ -1119,6 +1128,7 @@
 
             self._fromiterable(w_seq)
 
+        @sandbox_review(reviewed=True)
         def extend(self, w_iterable, accept_different_array=False):
             space = self.space
             if isinstance(w_iterable, W_Array):
@@ -1170,6 +1180,7 @@
 
         # interface
 
+        @sandbox_review(reviewed=True)
         def descr_append(self, space, w_x):
             x = self.item_w(w_x)
             index = self.len
@@ -1179,12 +1190,14 @@
 
         # List interface
 
+        @sandbox_review(reviewed=True)
         def descr_reverse(self, space):
             b = self.get_buffer()
             for i in range(self.len / 2):
                 b[i], b[self.len - i - 1] = b[self.len - i - 1], b[i]
             keepalive_until_here(self)
 
+        @sandbox_review(reviewed=True)
         def descr_pop(self, space, i):
             if i < 0:
                 i += self.len
@@ -1199,6 +1212,7 @@
             self.setlen(self.len - 1)
             return w_val
 
+        @sandbox_review(reviewed=True)
         def descr_insert(self, space, idx, w_val):
             if idx < 0:
                 idx += self.len
@@ -1217,6 +1231,7 @@
             b[i] = val
             keepalive_until_here(self)
 
+        @sandbox_review(reviewed=True)
         def getitem_slice(self, space, w_idx):
             start, stop, step, size = space.decode_index4(w_idx, self.len)
             w_a = mytype.w_class(self.space)
@@ -1232,6 +1247,7 @@
             keepalive_until_here(w_a)
             return w_a
 
+        @sandbox_review(reviewed=True)
         def setitem(self, space, w_idx, w_item):
             idx, stop, step = space.decode_index(w_idx, self.len)
             if step != 0:
@@ -1241,6 +1257,7 @@
             self.get_buffer()[idx] = item
             keepalive_until_here(self)
 
+        @sandbox_review(reviewed=True)
         def setitem_slice(self, space, w_idx, w_item):
             if not isinstance(w_item, W_Array):
                 raise oefmt(space.w_TypeError,
@@ -1268,6 +1285,7 @@
                 keepalive_until_here(w_item)
                 keepalive_until_here(self)
 
+        @sandbox_review(check_caller=True)
         def _repeat_single_item(self, a, start, repeat):
             # <a performance hack>
             assert isinstance(a, W_Array)
diff --git a/pypy/module/gc/interp_gc.py b/pypy/module/gc/interp_gc.py
--- a/pypy/module/gc/interp_gc.py
+++ b/pypy/module/gc/interp_gc.py
@@ -46,7 +46,8 @@
     If they were already enabled, no-op.
     If they were disabled even several times, enable them anyway.
     """
-    rgc.enable()
+    if not space.config.translation.sandbox:    # not available in sandbox
+        rgc.enable()
     if not space.user_del_action.enabled_at_app_level:
         space.user_del_action.enabled_at_app_level = True
         enable_finalizers(space)
@@ -55,7 +56,8 @@
     """Non-recursive version.  Disable major collections and finalizers.
     Multiple calls to this function are ignored.
     """
-    rgc.disable()
+    if not space.config.translation.sandbox:    # not available in sandbox
+        rgc.disable()
     if space.user_del_action.enabled_at_app_level:
         space.user_del_action.enabled_at_app_level = False
         disable_finalizers(space)
diff --git a/pypy/module/time/interp_time.py b/pypy/module/time/interp_time.py
--- a/pypy/module/time/interp_time.py
+++ b/pypy/module/time/interp_time.py
@@ -12,6 +12,7 @@
 from rpython.rlib.rtime import (GETTIMEOFDAY_NO_TZ, TIMEVAL,
                                 HAVE_GETTIMEOFDAY, HAVE_FTIME)
 from rpython.rlib import rposix, rtime
+from rpython.rlib.objectmodel import sandbox_review
 from rpython.translator.tool.cbuild import ExternalCompilationInfo
 import math
 import os
@@ -365,6 +366,7 @@
 c_strftime = external('strftime', [rffi.CCHARP, rffi.SIZE_T, rffi.CCHARP, TM_P],
                       rffi.SIZE_T, sandboxsafe=True)
 
+ at sandbox_review(reviewed=True)
 def _init_timezone(space):
     timezone = daylight = altzone = 0
     tzname = ["", ""]
@@ -564,6 +566,7 @@
     w_obj = space.call_function(w_struct_time, w_time_tuple)
     return w_obj
 
+ at sandbox_review(reviewed=True)
 def _gettmarg(space, w_tup, allowNone=True):
     if space.is_none(w_tup):
         if not allowNone:
@@ -679,6 +682,7 @@
                 return space.newfloat(_timespec_to_seconds(timespec))
     return gettimeofday(space, w_info)
 
+ at sandbox_review(reviewed=True)
 def ctime(space, w_seconds=None):
     """ctime([seconds]) -> string
 
@@ -725,6 +729,7 @@
     return space.mod(space.newtext("%.3s %.3s%3d %.2d:%.2d:%.2d %d"),
                      space.newtuple(args))
 
+ at sandbox_review(reviewed=True)
 def gmtime(space, w_seconds=None):
     """gmtime([seconds]) -> (tm_year, tm_mon, tm_day, tm_hour, tm_min,
                           tm_sec, tm_wday, tm_yday, tm_isdst)
@@ -746,6 +751,7 @@
                              space.newtext(*_get_error_msg()))
     return _tm_to_tuple(space, p)
 
+ at sandbox_review(reviewed=True)
 def localtime(space, w_seconds=None):
     """localtime([seconds]) -> (tm_year, tm_mon, tm_day, tm_hour, tm_min,
                              tm_sec, tm_wday, tm_yday, tm_isdst)
@@ -764,6 +770,7 @@
                              space.newtext(*_get_error_msg()))
     return _tm_to_tuple(space, p)
 
+ at sandbox_review(reviewed=True)
 def mktime(space, w_tup):
     """mktime(tuple) -> floating point number
 
diff --git a/rpython/memory/gc/incminimark.py b/rpython/memory/gc/incminimark.py
--- a/rpython/memory/gc/incminimark.py
+++ b/rpython/memory/gc/incminimark.py
@@ -1176,6 +1176,8 @@
 
 
     def unpin(self, obj):
+        if self.safer_variant():
+            out_of_memory("unpin() unexpected")
         ll_assert(self._is_pinned(obj),
             "unpin: object is already not pinned")
         #
@@ -1186,6 +1188,8 @@
         return (self.header(obj).tid & GCFLAG_PINNED) != 0
 
     def shrink_array(self, obj, smallerlength):
+        if self.safer_variant():    # no shrinking in the safer variant
+            return False       # (because the original 'obj' is kind of broken)
         #
         # Only objects in the nursery can be "resized".  Resizing them
         # means recording that they have a smaller size, so that when
diff --git a/rpython/memory/gc/inspector.py b/rpython/memory/gc/inspector.py
--- a/rpython/memory/gc/inspector.py
+++ b/rpython/memory/gc/inspector.py
@@ -89,7 +89,7 @@
 raw_os_write = rffi.llexternal(rposix.UNDERSCORE_ON_WIN32 + 'write',
                                [rffi.INT, llmemory.Address, rffi.SIZE_T],
                                rffi.SIZE_T,
-                               sandboxsafe=True, _nowrapper=True)
+                               _nowrapper=True)
 
 AddressStack = get_address_stack()
 
diff --git a/rpython/rlib/buffer.py b/rpython/rlib/buffer.py
--- a/rpython/rlib/buffer.py
+++ b/rpython/rlib/buffer.py
@@ -7,6 +7,7 @@
 from rpython.rtyper.lltypesystem.rlist import LIST_OF
 from rpython.rtyper.annlowlevel import llstr
 from rpython.rlib.objectmodel import specialize, we_are_translated
+from rpython.rlib.objectmodel import sandbox_review
 from rpython.rlib import jit
 from rpython.rlib.rgc import (resizable_list_supporting_raw_ptr,
                               nonmoving_raw_ptr_for_resizable_list,
@@ -143,6 +144,7 @@
         ptr = self.get_raw_address()
         return llop.raw_load(TP, ptr, byte_offset)
 
+    @sandbox_review(check_caller=True)
     @specialize.ll_and_arg(1)
     def typed_write(self, TP, byte_offset, value):
         """
@@ -179,6 +181,7 @@
         base_ofs = targetcls._get_gc_data_offset()
         scale_factor = llmemory.sizeof(lltype.Char)
 
+        @sandbox_review(check_caller=True)
         @specialize.ll_and_arg(1)
         def typed_read(self, TP, byte_offset):
             if not is_alignment_correct(TP, byte_offset):
@@ -188,6 +191,7 @@
             return llop.gc_load_indexed(TP, lldata, byte_offset,
                                         scale_factor, base_ofs)
 
+        @sandbox_review(check_caller=True)
         @specialize.ll_and_arg(1)
         def typed_write(self, TP, byte_offset, value):
             if self.readonly or not is_alignment_correct(TP, byte_offset):
@@ -362,10 +366,12 @@
         ptr = self.buffer.get_raw_address()
         return rffi.ptradd(ptr, self.offset)
 
+    @sandbox_review(check_caller=True)
     @specialize.ll_and_arg(1)
     def typed_read(self, TP, byte_offset):
         return self.buffer.typed_read(TP, byte_offset + self.offset)
 
+    @sandbox_review(check_caller=True)
     @specialize.ll_and_arg(1)
     def typed_write(self, TP, byte_offset, value):
         return self.buffer.typed_write(TP, byte_offset + self.offset, value)
diff --git a/rpython/rlib/objectmodel.py b/rpython/rlib/objectmodel.py
--- a/rpython/rlib/objectmodel.py
+++ b/rpython/rlib/objectmodel.py
@@ -226,6 +226,38 @@
     func._not_rpython_ = True
     return func
 
+def sandbox_review(reviewed=False, check_caller=False, abort=False):
+    """Mark a function as reviewed for sandboxing purposes.
+    This should not be necessary on any function written in "normal" RPython
+    code, but only on functions using some lloperation that is not
+    whitelisted in rpython.translator.sandbox.graphchecker.
+
+    Call this with one of the three flags set to True:
+
+      *reviewed*: This function is fine and any other code can call it.
+      If the function contains external calls, they will still be replaced with
+      stubs using I/O to communicate with the parent process (as long as they
+      are not marked sandboxsafe themselves).
+
+      *check_caller*: This function is fine, but you should still check the
+      callers; they must all have a sandbox_review() as well.
+
+      *abort*: An abort is prepended to the function's code, making the
+      whole process abort if it is called at runtime.
+
+    """
+    assert reviewed + check_caller + abort == 1
+    def wrap(func):
+        assert not hasattr(func, '_sandbox_review_') or abort
+        if reviewed:
+            func._sandbox_review_ = 'reviewed'
+        if check_caller:
+            func._sandbox_review_ = 'check_caller'
+        if abort:
+            func._sandbox_review_ = 'abort'
+        return func
+    return wrap
+
 
 # ____________________________________________________________
 
@@ -347,6 +379,10 @@
     # XXX this can be made more efficient in the future
     return bytearray(str(i))
 
+def sandboxed_translation():
+    config = fetch_translated_config()
+    return config is not None and config.translation.sandbox
+
 def fetch_translated_config():
     """Returns the config that is current when translating.
     Returns None if not translated.
diff --git a/rpython/rlib/rgc.py b/rpython/rlib/rgc.py
--- a/rpython/rlib/rgc.py
+++ b/rpython/rlib/rgc.py
@@ -6,6 +6,7 @@
 from rpython.rlib import jit
 from rpython.rlib.objectmodel import we_are_translated, enforceargs, specialize
 from rpython.rlib.objectmodel import CDefinedIntSymbolic, not_rpython
+from rpython.rlib.objectmodel import sandbox_review, sandboxed_translation
 from rpython.rtyper.extregistry import ExtRegistryEntry
 from rpython.rtyper.lltypesystem import lltype, llmemory
 
@@ -358,14 +359,23 @@
             return True
     return False
 
+ at not_rpython
+def _ll_arraycopy_of_nongc_not_for_sandboxed():
+    pass
 
 @jit.oopspec('list.ll_arraycopy(source, dest, source_start, dest_start, length)')
 @enforceargs(None, None, int, int, int)
+ at sandbox_review(reviewed=True)
 @specialize.ll()
 def ll_arraycopy(source, dest, source_start, dest_start, length):
     from rpython.rtyper.lltypesystem.lloperation import llop
     from rpython.rlib.objectmodel import keepalive_until_here
 
+    TP = lltype.typeOf(source).TO
+    assert TP == lltype.typeOf(dest).TO
+    if TP._gckind != 'gc' and sandboxed_translation():
+        _ll_arraycopy_of_nongc_not_for_sandboxed()
+
     # XXX: Hack to ensure that we get a proper effectinfo.write_descrs_arrays
     # and also, maybe, speed up very small cases
     if length <= 1:
@@ -379,9 +389,6 @@
             assert (source_start + length <= dest_start or
                     dest_start + length <= source_start)
 
-    TP = lltype.typeOf(source).TO
-    assert TP == lltype.typeOf(dest).TO
-
     slowpath = False
     if must_split_gc_address_space():
         slowpath = True
@@ -415,6 +422,7 @@
 
 @jit.oopspec('rgc.ll_shrink_array(p, smallerlength)')
 @enforceargs(None, int)
+ at sandbox_review(reviewed=True)
 @specialize.ll()
 def ll_shrink_array(p, smallerlength):
     from rpython.rtyper.lltypesystem.lloperation import llop
@@ -454,6 +462,7 @@
     return newp
 
 @jit.dont_look_inside
+ at sandbox_review(reviewed=True)
 @specialize.ll()
 def ll_arrayclear(p):
     # Equivalent to memset(array, 0).  Only for GcArray(primitive-type) for now.
@@ -1096,6 +1105,7 @@
         hop.exception_cannot_occur()
         return hop.genop('gc_gcflag_extra', vlist, resulttype = hop.r_result)
 
+ at specialize.memo()
 def lltype_is_gc(TP):
     return getattr(getattr(TP, "TO", None), "_gckind", "?") == 'gc'
 
@@ -1419,7 +1429,7 @@
     return _ResizableListSupportingRawPtr(lst)
 
 def nonmoving_raw_ptr_for_resizable_list(lst):
-    if must_split_gc_address_space():
+    if must_split_gc_address_space() or sandboxed_translation():
         raise ValueError
     return _nonmoving_raw_ptr_for_resizable_list(lst)
 
@@ -1501,6 +1511,7 @@
 
 
 @jit.dont_look_inside
+ at sandbox_review(check_caller=True)
 def ll_nonmovable_raw_ptr_for_resizable_list(ll_list):
     """
     WARNING: dragons ahead.
diff --git a/rpython/rlib/rposix.py b/rpython/rlib/rposix.py
--- a/rpython/rlib/rposix.py
+++ b/rpython/rlib/rposix.py
@@ -10,7 +10,8 @@
     _CYGWIN, _MACRO_ON_POSIX, UNDERSCORE_ON_WIN32, _WIN32,
     _prefer_unicode, _preferred_traits, _preferred_traits2)
 from rpython.rlib.objectmodel import (
-    specialize, enforceargs, register_replacement_for, NOT_CONSTANT)
+    specialize, enforceargs, register_replacement_for, NOT_CONSTANT,
+    sandbox_review)
 from rpython.rlib.rarithmetic import intmask, widen
 from rpython.rlib.signature import signature
 from rpython.tool.sourcetools import func_renamer
@@ -988,6 +989,7 @@
                      [rffi.INTP, rffi.VOIDP, rffi.VOIDP, rffi.VOIDP],
                      rffi.PID_T, _nowrapper = True)
 
+ at sandbox_review(abort=True)
 @replace_os_function('fork')
 @jit.dont_look_inside
 def fork():
@@ -1017,6 +1019,7 @@
         lltype.free(master_p, flavor='raw')
         lltype.free(slave_p, flavor='raw')
 
+ at sandbox_review(abort=True)
 @replace_os_function('forkpty')
 @jit.dont_look_inside
 def forkpty():
@@ -1058,6 +1061,7 @@
                          [rffi.PID_T, rffi.INTP, rffi.INT], rffi.PID_T,
                          save_err=rffi.RFFI_SAVE_ERRNO)
 
+ at sandbox_review(reviewed=True)
 @replace_os_function('waitpid')
 def waitpid(pid, options):
     status_p = lltype.malloc(rffi.INTP.TO, 1, flavor='raw')
@@ -1743,6 +1747,7 @@
     finally:
         lltype.free(groups, flavor='raw')
 
+ at sandbox_review(reviewed=True)
 @replace_os_function('setgroups')
 def setgroups(gids):
     n = len(gids)
diff --git a/rpython/rlib/rstack.py b/rpython/rlib/rstack.py
--- a/rpython/rlib/rstack.py
+++ b/rpython/rlib/rstack.py
@@ -6,6 +6,7 @@
 import py
 
 from rpython.rlib.objectmodel import we_are_translated, fetch_translated_config
+from rpython.rlib.objectmodel import sandbox_review
 from rpython.rlib.rarithmetic import r_uint
 from rpython.rlib import rgc
 from rpython.rtyper.lltypesystem import lltype, rffi
@@ -15,7 +16,7 @@
 
 def llexternal(name, args, res, _callable=None):
     return rffi.llexternal(name, args, res,
-                           sandboxsafe=True, _nowrapper=True,
+                           sandboxsafe='check_caller', _nowrapper=True,
                            _callable=_callable)
 
 _stack_get_end = llexternal('LL_stack_get_end', [], lltype.Signed,
@@ -39,6 +40,7 @@
 _stack_criticalcode_stop = llexternal('LL_stack_criticalcode_stop', [],
                                       lltype.Void, lambda: None)
 
+ at sandbox_review(reviewed=True)
 def stack_check():
     if not we_are_translated():
         return
@@ -64,6 +66,7 @@
 stack_check._always_inline_ = True
 stack_check._dont_insert_stackcheck_ = True
 
+ at sandbox_review(check_caller=True)
 @rgc.no_collect
 def stack_check_slowpath(current):
     if ord(_stack_too_big_slowpath(current)):
@@ -72,6 +75,7 @@
 stack_check_slowpath._dont_inline_ = True
 stack_check_slowpath._dont_insert_stackcheck_ = True
 
+ at sandbox_review(reviewed=True)
 def stack_almost_full():
     """Return True if the stack is more than 15/16th full."""
     if not we_are_translated():
diff --git a/rpython/rlib/rstruct/standardfmttable.py b/rpython/rlib/rstruct/standardfmttable.py
--- a/rpython/rlib/rstruct/standardfmttable.py
+++ b/rpython/rlib/rstruct/standardfmttable.py
@@ -7,7 +7,7 @@
 
 import struct
 
-from rpython.rlib.objectmodel import specialize
+from rpython.rlib.objectmodel import specialize, sandbox_review
 from rpython.rlib.rarithmetic import r_uint, r_longlong, r_ulonglong
 from rpython.rlib.rstruct import ieee
 from rpython.rlib.rstruct.error import StructError, StructOverflowError
@@ -30,6 +30,7 @@
     Create a fast path packer for TYPE. The packer returns True is it succeded
     or False otherwise.
     """
+    @sandbox_review(reviewed=True)
     @specialize.argtype(0)
     def do_pack_fastpath(fmtiter, value):
         size = rffi.sizeof(TYPE)
@@ -39,6 +40,7 @@
             raise CannotWrite
         #
         # typed_write() might raise CannotWrite
+        # (note that we assume the write cannot overflow its buffer)
         fmtiter.wbuf.typed_write(TYPE, fmtiter.pos, value)
         if not ALLOW_FASTPATH:
             # if we are here it means that typed_write did not raise, and thus
@@ -211,6 +213,7 @@
 
 @specialize.memo()
 def unpack_fastpath(TYPE):
+    @sandbox_review(reviewed=True)
     @specialize.argtype(0)
     def do_unpack_fastpath(fmtiter):
         size = rffi.sizeof(TYPE)
@@ -289,9 +292,15 @@
             # because of alignment issues. So we copy the slice into a new
             # string, which is guaranteed to be properly aligned, and read the
             # float/double from there
-            input = fmtiter.read(size)
-            val = StringBuffer(input).typed_read(TYPE, 0)
+            val = read_slowpath(fmtiter)
         fmtiter.appendobj(float(val))
+
+    @sandbox_review(reviewed=True)
+    def read_slowpath(fmtiter):
+        size = rffi.sizeof(TYPE)
+        input = fmtiter.read(size)
+        return StringBuffer(input).typed_read(TYPE, 0)
+
     return unpack_ieee
 
 @specialize.argtype(0)
diff --git a/rpython/rlib/rthread.py b/rpython/rlib/rthread.py
--- a/rpython/rlib/rthread.py
+++ b/rpython/rlib/rthread.py
@@ -6,6 +6,7 @@
 from rpython.rlib.debug import ll_assert
 from rpython.rlib.objectmodel import we_are_translated, specialize
 from rpython.rlib.objectmodel import CDefinedIntSymbolic, not_rpython
+from rpython.rlib.objectmodel import sandbox_review
 from rpython.rtyper.lltypesystem.lloperation import llop
 from rpython.rtyper.tool import rffi_platform
 from rpython.rtyper.extregistry import ExtRegistryEntry
@@ -225,7 +226,7 @@
 
 get_stacksize = llexternal('RPyThreadGetStackSize', [], lltype.Signed)
 set_stacksize = llexternal('RPyThreadSetStackSize', [lltype.Signed],
-                           lltype.Signed)
+                           lltype.Signed, sandboxsafe='abort')
 
 # ____________________________________________________________
 #
diff --git a/rpython/rlib/rtime.py b/rpython/rlib/rtime.py
--- a/rpython/rlib/rtime.py
+++ b/rpython/rlib/rtime.py
@@ -8,7 +8,7 @@
 from rpython.translator.tool.cbuild import ExternalCompilationInfo
 from rpython.rtyper.tool import rffi_platform
 from rpython.rtyper.lltypesystem import rffi, lltype
-from rpython.rlib.objectmodel import register_replacement_for
+from rpython.rlib.objectmodel import register_replacement_for, sandbox_review
 from rpython.rlib.rarithmetic import intmask, UINT_MAX
 from rpython.rlib import rposix
 
@@ -262,6 +262,7 @@
                                    lltype.Ptr(TIMEVAL)], rffi.INT,
                         save_err=rffi.RFFI_SAVE_ERRNO)
 
+ at sandbox_review(reviewed=True)
 @replace_time_function('sleep')
 def sleep(secs):
     if _WIN32:
diff --git a/rpython/rtyper/lltypesystem/rffi.py b/rpython/rtyper/lltypesystem/rffi.py
--- a/rpython/rtyper/lltypesystem/rffi.py
+++ b/rpython/rtyper/lltypesystem/rffi.py
@@ -9,6 +9,7 @@
 from rpython.tool.sourcetools import func_with_new_name
 from rpython.rlib.objectmodel import Symbolic, specialize, not_rpython
 from rpython.rlib.objectmodel import keepalive_until_here, enforceargs
+from rpython.rlib.objectmodel import sandbox_review
 from rpython.rlib import rarithmetic, rgc
 from rpython.rtyper.extregistry import ExtRegistryEntry
 from rpython.rlib.unroll import unrolling_iterable
@@ -97,6 +98,12 @@
                 don't bother releasing the GIL.  An explicit True or False
                 overrides this logic.
 
+    sandboxsafe: if True, the process really calls the C function even if it
+                 is sandboxed.  If False, it will turn into a stdin/stdout
+                 communication with the parent process.  If "check_caller",
+                 it is like True but we call @sandbox_review(check_caller=True)
+                 which means that we need to also check the callers.
+
     calling_conv: if 'unknown' or 'win', the C function is not directly seen
                   by the JIT.  If 'c', it can be seen (depending on
                   releasegil=False).  For tests only, or if _nowrapper,
@@ -214,6 +221,8 @@
         #
         call_external_function = func_with_new_name(call_external_function,
                                                     'ccall_' + name)
+        call_external_function = sandbox_review(check_caller=True)(
+            call_external_function)
         # don't inline, as a hack to guarantee that no GC pointer is alive
         # anywhere in call_external_function
     else:
@@ -251,6 +260,8 @@
                                                         'ccall_' + name)
             call_external_function = jit.dont_look_inside(
                 call_external_function)
+            call_external_function = sandbox_review(check_caller=True)(
+                call_external_function)
 
     def _oops():
         raise AssertionError("can't pass (any more) a unicode string"
@@ -329,8 +340,18 @@
     # for debugging, stick ll func ptr to that
     wrapper._ptr = funcptr
     wrapper = func_with_new_name(wrapper, name)
+    if sandboxsafe == 'check_caller':
+        wrapper = sandbox_review(check_caller=True)(wrapper)
+    elif sandboxsafe == 'abort':
+        wrapper = sandbox_review(abort=True)(wrapper)
+    else:
+        assert isinstance(sandboxsafe, bool)
+        wrapper = sandbox_review(reviewed=True)(wrapper)
     return wrapper
 
+def sandbox_check_type(TYPE):
+    return not isinstance(TYPE, lltype.Primitive) or TYPE == llmemory.Address
+
 
 class CallbackHolder:
     def __init__(self):
@@ -792,6 +813,7 @@
         lastchar = u'\x00'
 
     # str -> char*
+    @sandbox_review(reviewed=True)
     def str2charp(s, track_allocation=True):
         """ str -> char*
         """
@@ -806,6 +828,7 @@
         return array
     str2charp._annenforceargs_ = [strtype, bool]
 
+    @sandbox_review(reviewed=True)
     def free_charp(cp, track_allocation=True):
         if track_allocation:
             lltype.free(cp, flavor='raw', track_allocation=True)
@@ -838,6 +861,7 @@
 
     # str -> (buf, llobj, flag)
     # Can't inline this because of the raw address manipulation.
+    @sandbox_review(reviewed=True)
     @jit.dont_look_inside
     def get_nonmovingbuffer_ll(data):
         """
@@ -891,6 +915,7 @@
     get_nonmovingbuffer_ll._annenforceargs_ = [strtype]
 
 
+    @sandbox_review(reviewed=True)
     @jit.dont_look_inside
     def get_nonmovingbuffer_ll_final_null(data):
         tup = get_nonmovingbuffer_ll(data)
@@ -902,6 +927,7 @@
 
     # args-from-tuple-returned-by-get_nonmoving_buffer() -> None
     # Can't inline this because of the raw address manipulation.
+    @sandbox_review(reviewed=True)
     @jit.dont_look_inside
     def free_nonmovingbuffer_ll(buf, llobj, flag):
         """
@@ -918,6 +944,7 @@
 
     # int -> (char*, str, int)
     # Can't inline this because of the raw address manipulation.
+    @sandbox_review(reviewed=True)
     @jit.dont_look_inside
     def alloc_buffer(count):
         """
@@ -1096,6 +1123,7 @@
 CCHARPP = lltype.Ptr(lltype.Array(CCHARP, hints={'nolength': True}))
 CWCHARPP = lltype.Ptr(lltype.Array(CWCHARP, hints={'nolength': True}))
 
+ at sandbox_review(reviewed=True)
 def liststr2charpp(l):
     """ list[str] -> char**, NULL terminated
     """
@@ -1241,6 +1269,7 @@
         return v_ptr
 
 
+ at sandbox_review(check_caller=True)
 def structcopy(pdst, psrc):
     """Copy all the fields of the structure given by 'psrc'
     into the structure given by 'pdst'.
@@ -1258,6 +1287,7 @@
                                              if name not in padding]
         unrollfields = unrolling_iterable(fields)
 
+        @sandbox_review(check_caller=True)
         def copyfn(pdst, psrc):
             for name, TYPE in unrollfields:
                 if isinstance(TYPE, lltype.ContainerType):
@@ -1271,6 +1301,7 @@
 _get_structcopy_fn._annspecialcase_ = 'specialize:memo'
 
 
+ at sandbox_review(check_caller=True)
 def setintfield(pdst, fieldname, value):
     """Maybe temporary: a helper to set an integer field into a structure,
     transparently casting between the various integer types.
@@ -1405,14 +1436,14 @@
             lltype.Void,
             releasegil=False,
             calling_conv='c',
-            sandboxsafe=True,
+            sandboxsafe='check_caller',
         )
 c_memset = llexternal("memset",
             [VOIDP, lltype.Signed, SIZE_T],
             lltype.Void,
             releasegil=False,
             calling_conv='c',
-            sandboxsafe=True,
+            sandboxsafe='check_caller',
         )
 
 
diff --git a/rpython/rtyper/lltypesystem/rstr.py b/rpython/rtyper/lltypesystem/rstr.py
--- a/rpython/rtyper/lltypesystem/rstr.py
+++ b/rpython/rtyper/lltypesystem/rstr.py
@@ -3,7 +3,8 @@
 from rpython.annotator import model as annmodel
 from rpython.rlib import jit, types, objectmodel, rgc
 from rpython.rlib.objectmodel import (malloc_zero_filled, we_are_translated,
-    ll_hash_string, keepalive_until_here, specialize, enforceargs, dont_inline)
+    ll_hash_string, keepalive_until_here, specialize, enforceargs, dont_inline,
+    sandbox_review)
 from rpython.rlib.signature import signature
 from rpython.rlib.rarithmetic import ovfcheck
 from rpython.rtyper.error import TyperError
@@ -59,6 +60,7 @@
                 llmemory.itemoffsetof(TP.chars, 0) +
                 llmemory.sizeof(CHAR_TP) * item)
 
+    @sandbox_review(check_caller=True)
     @signature(types.any(), types.any(), types.int(), returns=types.any())
     @specialize.arg(0)
     def _get_raw_buf(TP, src, ofs):
@@ -75,6 +77,7 @@
     _get_raw_buf._always_inline_ = True
 
     @jit.oopspec('stroruni.copy_contents(src, dst, srcstart, dststart, length)')
+    @sandbox_review(reviewed=True)
     @signature(types.any(), types.any(), types.int(), types.int(), types.int(), returns=types.none())
     def copy_string_contents(src, dst, srcstart, dststart, length):
         """Copies 'length' characters from the 'src' string to the 'dst'
@@ -112,6 +115,7 @@
     copy_string_contents = func_with_new_name(copy_string_contents,
                                               'copy_%s_contents' % name)
 
+    @sandbox_review(check_caller=True)
     @jit.oopspec('stroruni.copy_string_to_raw(src, ptrdst, srcstart, length)')
     def copy_string_to_raw(src, ptrdst, srcstart, length):
         """
@@ -141,6 +145,7 @@
     copy_string_to_raw._always_inline_ = True
     copy_string_to_raw = func_with_new_name(copy_string_to_raw, 'copy_%s_to_raw' % name)
 
+    @sandbox_review(reviewed=True)
     @jit.dont_look_inside
     @signature(types.any(), types.any(), types.int(), types.int(),
                returns=types.none())
@@ -1258,6 +1263,7 @@
         return hop.gendirectcall(cls.ll_join_strs, size, vtemp)
 
     @staticmethod
+    @sandbox_review(reviewed=True)
     @jit.dont_look_inside
     def ll_string2list(RESLIST, src):
         length = len(src.chars)
diff --git a/rpython/translator/backendopt/all.py b/rpython/translator/backendopt/all.py
--- a/rpython/translator/backendopt/all.py
+++ b/rpython/translator/backendopt/all.py
@@ -113,7 +113,7 @@
     if config.profile_based_inline and not secondary:
         threshold = config.profile_based_inline_threshold
         heuristic = get_function(config.profile_based_inline_heuristic)
-        inline.instrument_inline_candidates(graphs, threshold)
+        inline.instrument_inline_candidates(translator, graphs, threshold)
         counters = translator.driver_instrument_result(
             config.profile_based_inline)
         n = len(counters)
diff --git a/rpython/translator/backendopt/inline.py b/rpython/translator/backendopt/inline.py
--- a/rpython/translator/backendopt/inline.py
+++ b/rpython/translator/backendopt/inline.py
@@ -548,7 +548,8 @@
     return (0.9999 * measure_median_execution_cost(graph) +
             count), True       # may be NaN
 
-def inlinable_static_callers(graphs, store_calls=False, ok_to_call=None):
+def inlinable_static_callers(translator, graphs, store_calls=False,
+                             ok_to_call=None):
     if ok_to_call is None:
         ok_to_call = set(graphs)
     result = []
@@ -558,6 +559,7 @@
         else:
             result.append((parentgraph, graph))
     #
+    dont_inline = make_dont_inline_checker(translator)
     for parentgraph in graphs:
         for block in parentgraph.iterblocks():
             for op in block.operations:
@@ -565,13 +567,12 @@
                     funcobj = op.args[0].value._obj
                     graph = getattr(funcobj, 'graph', None)
                     if graph is not None and graph in ok_to_call:
-                        if getattr(getattr(funcobj, '_callable', None),
-                                   '_dont_inline_', False):
+                        if dont_inline(funcobj):
                             continue
                         add(parentgraph, block, op, graph)
     return result
 
-def instrument_inline_candidates(graphs, threshold):
+def instrument_inline_candidates(translator, graphs, threshold):
     cache = {None: False}
     def candidate(graph):
         try:
@@ -581,6 +582,7 @@
             cache[graph] = res
             return res
     n = 0
+    dont_inline = make_dont_inline_checker(translator)
     for parentgraph in graphs:
         for block in parentgraph.iterblocks():
             ops = block.operations
@@ -592,8 +594,7 @@
                     funcobj = op.args[0].value._obj
                     graph = getattr(funcobj, 'graph', None)
                     if graph is not None:
-                        if getattr(getattr(funcobj, '_callable', None),
-                                   '_dont_inline_', False):
+                        if dont_inline(funcobj):
                             continue
                     if candidate(graph):
                         tag = Constant('inline', Void)
@@ -610,6 +611,17 @@
     return (hasattr(graph, 'func') and
             getattr(graph.func, '_always_inline_', None))
 
+def make_dont_inline_checker(translator):
+    sandbox = translator.config.translation.sandbox
+
+    def dont_inline(funcobj):
+        func = getattr(funcobj, '_callable', None)
+        if sandbox:
+            if hasattr(func, '_sandbox_review_'):
+                return True
+        return getattr(func, '_dont_inline_', False)
+    return dont_inline
+
 def auto_inlining(translator, threshold=None,
                   callgraph=None,
                   call_count_pred=None,
@@ -621,7 +633,7 @@
     callers = {}     # {graph: {graphs-that-call-it}}
     callees = {}     # {graph: {graphs-that-it-calls}}
     if callgraph is None:
-        callgraph = inlinable_static_callers(translator.graphs)
+        callgraph = inlinable_static_callers(translator, translator.graphs)
     for graph1, graph2 in callgraph:
         callers.setdefault(graph2, {})[graph1] = True
         callees.setdefault(graph1, {})[graph2] = True
@@ -727,7 +739,8 @@
                                 if not hasattr(graph, 'exceptiontransformed')])
     else:
         ok_to_call = None
-    callgraph = inlinable_static_callers(graphs, ok_to_call=ok_to_call)
+    callgraph = inlinable_static_callers(translator, graphs,
+                                         ok_to_call=ok_to_call)
     count = auto_inlining(translator, threshold, callgraph=callgraph,
                           heuristic=heuristic,
                           call_count_pred=call_count_pred)
diff --git a/rpython/translator/backendopt/test/test_inline.py b/rpython/translator/backendopt/test/test_inline.py
--- a/rpython/translator/backendopt/test/test_inline.py
+++ b/rpython/translator/backendopt/test/test_inline.py
@@ -100,7 +100,7 @@
         call_count_pred = None
         if call_count_check:
             call_count_pred = lambda lbl: True
-            instrument_inline_candidates(t.graphs, threshold)
+            instrument_inline_candidates(t, t.graphs, threshold)
 
         if remove_same_as:
             for graph in t.graphs:
diff --git a/rpython/translator/c/genc.py b/rpython/translator/c/genc.py
--- a/rpython/translator/c/genc.py
+++ b/rpython/translator/c/genc.py
@@ -65,6 +65,11 @@
 
     def __init__(self, translator, entrypoint, config, gcpolicy=None,
                  gchooks=None, secondary_entrypoints=()):
+        #
+        if config.translation.sandbox:
+            assert not config.translation.thread
+            gchooks = None     # no custom gc hooks
+        #
         self.translator = translator
         self.entrypoint = entrypoint
         self.entrypoint_name = getattr(self.entrypoint, 'func_name', None)
diff --git a/rpython/translator/c/node.py b/rpython/translator/c/node.py
--- a/rpython/translator/c/node.py
+++ b/rpython/translator/c/node.py
@@ -882,9 +882,9 @@
 
 def new_funcnode(db, T, obj, forcename=None):
     from rpython.rtyper.rtyper import llinterp_backend
-    if db.sandbox:
-        if (getattr(obj, 'external', None) is not None and
-                not obj._safe_not_sandboxed):
+    if db.sandbox and getattr(obj, 'external', None) is not None:
+        safe_flag = obj._safe_not_sandboxed
+        if not (safe_flag is True or safe_flag == "check_caller"):
             try:
                 sandbox_mapping = db.sandbox_mapping
             except AttributeError:
diff --git a/rpython/translator/driver.py b/rpython/translator/driver.py
--- a/rpython/translator/driver.py
+++ b/rpython/translator/driver.py
@@ -412,6 +412,10 @@
         if translator.annotator is not None:
             translator.frozen = True
 
+        if self.config.translation.sandbox:
+            from rpython.translator.sandbox import graphchecker
+            graphchecker.check_all_graphs(self.translator)
+
         standalone = self.standalone
         get_gchooks = self.extra.get('get_gchooks', lambda: None)
         gchooks = get_gchooks()
diff --git a/rpython/translator/sandbox/graphchecker.py b/rpython/translator/sandbox/graphchecker.py
new file mode 100644
--- /dev/null
+++ b/rpython/translator/sandbox/graphchecker.py
@@ -0,0 +1,138 @@
+"""Logic to check the operations in all the user graphs.
+This runs at the start of the database-c step, so it excludes the
+graphs produced later, notably for the GC.  These are "low-level"
+graphs that are assumed to be safe.
+"""
+
+from rpython.flowspace.model import SpaceOperation, Constant
+from rpython.rtyper.rmodel import inputconst
+from rpython.rtyper.lltypesystem import lltype, llmemory, rstr
+from rpython.rtyper.lltypesystem.lloperation import LL_OPERATIONS
+from rpython.translator.unsimplify import varoftype
+from rpython.tool.ansi_print import AnsiLogger
+
+class UnsafeException(Exception):
+    pass
+
+log = AnsiLogger("sandbox")
+
+safe_operations = set([
+    'keepalive', 'threadlocalref_get', 'threadlocalref_store',
+    'malloc', 'malloc_varsize', 'free',
+    'getfield', 'getarrayitem', 'getinteriorfield', 'raw_load',
+    'cast_opaque_ptr', 'cast_ptr_to_int',
+    'gc_thread_run', 'gc_stack_bottom', 'gc_thread_after_fork',
+    'shrink_array', 'gc_pin', 'gc_unpin', 'gc_can_move', 'gc_id',
+    'gc_identityhash', 'weakref_create', 'weakref_deref',
+    'gc_fq_register', 'gc_fq_next_dead',
+    'gc_set_max_heap_size', 'gc_ignore_finalizer', 'gc_add_memory_pressure',
+    'gc_writebarrier', 'gc__collect',
+    'length_of_simple_gcarray_from_opaque',
+    'debug_fatalerror', 'debug_print_traceback', 'debug_flush',
+    'hint', 'debug_start', 'debug_stop', 'debug_print', 'debug_offset',
+    'jit_force_quasi_immutable', 'jit_force_virtual', 'jit_marker',
+    'jit_is_virtual',
+    ])
+gc_set_operations = set([
+    'setfield', 'setarrayitem', 'setinteriorfield',
+    ])
+for opname, opdesc in LL_OPERATIONS.items():
+    if opdesc.tryfold:
+        safe_operations.add(opname)
+
+def graph_review(graph):
+    return getattr(getattr(graph, 'func', None), '_sandbox_review_', None)
+
+def make_abort_graph(graph):
+    ll_err = rstr.conststr("reached forbidden function %r" % (graph.name,))
+    c_err = inputconst(lltype.typeOf(ll_err), ll_err)
+    op = SpaceOperation('debug_fatalerror', [c_err], varoftype(lltype.Void))
+    graph.startblock.operations.insert(0, op)
+
+def is_gc_ptr(TYPE):
+    return isinstance(TYPE, lltype.Ptr) and TYPE.TO._gckind == 'gc'
+
+
+class GraphChecker(object):
+
+    def __init__(self, translator):
+        self.translator = translator
+
+    def graph_is_unsafe(self, graph):
+        for block, op in graph.iterblockops():
+            opname = op.opname
+
+            if opname in safe_operations:
+                pass
+
+            elif opname in gc_set_operations:
+                if op.args[0].concretetype.TO._gckind != 'gc':
+                    return "non-GC memory write: %r" % (op,)
+
+            elif opname == 'direct_call':
+                c_target = op.args[0]
+                assert isinstance(c_target, Constant)
+                TYPE = lltype.typeOf(c_target.value)
+                assert isinstance(TYPE.TO, lltype.FuncType)
+                obj = c_target.value._obj
+                if hasattr(obj, 'graph'):
+                    g2 = obj.graph
+                    if graph_review(g2) == 'check_caller':
+                        return ("direct_call to a graph with "
+                                "check_caller=True: %r" % (op,))
+                elif getattr(obj, '_safe_not_sandboxed', False) is not False:
+                    ss = obj._safe_not_sandboxed
+                    if ss is not True:
+                        return ("direct_call to llfunc with "
+                                "sandboxsafe=%r: %r" % (ss, obj))
+                elif getattr(obj, 'external', None) is not None:
+                    # either obj._safe_not_sandboxed is True, and then it's
+                    # fine; or obj._safe_not_sandboxed is False, and then
+                    # this will be transformed into a stdin/stdout stub
+                    pass
+                else:
+                    # not 'external', but no 'graph' either?
+                    return "direct_call to %r" % (obj,)
+
+            elif opname == 'indirect_call':
+                graph_list = op.args[-1].value
+                for g2 in graph_list:
+                    if graph_review(g2) == 'check_caller':
+                        return ("indirect_call that can go to at least one "
+                                "graph with check_caller=True: %r" % (op,))
+
+            elif opname in ('cast_ptr_to_adr', 'force_cast',
+                            'cast_int_to_ptr'):
+                if is_gc_ptr(op.args[0].concretetype):
+                    return "argument is a GC ptr: %r" % (opname,)
+                if is_gc_ptr(op.result.concretetype):
+                    return "result is a GC ptr: %r" % (opname,)
+
+            else:
+                return "unsupported llop: %r" % (opname,)
+
+    def check(self):
+        unsafe = {}
+        for graph in self.translator.graphs:
+            review = graph_review(graph)
+            if review is not None:
+                if review in ('reviewed', 'check_caller'):
+                    continue
+                elif review == 'abort':
+                    make_abort_graph(graph)
+                    continue
+                else:
+                    assert False, repr(review)
+
+            problem = self.graph_is_unsafe(graph)
+            if problem is not None:
+                unsafe[graph] = problem
+        if unsafe:
+            raise UnsafeException(
+                '\n'.join('%r: %s' % kv for kv in unsafe.items()))
+
+
+def check_all_graphs(translator):
+    log("Checking the graphs for sandbox-unsafe operations")
+    checker = GraphChecker(translator)
+    checker.check()
diff --git a/rpython/translator/sandbox/rsandbox.py b/rpython/translator/sandbox/rsandbox.py
--- a/rpython/translator/sandbox/rsandbox.py
+++ b/rpython/translator/sandbox/rsandbox.py
@@ -6,7 +6,7 @@
 import py
 import sys
 
-from rpython.rlib import types
+from rpython.rlib import types, debug
 from rpython.rlib.objectmodel import specialize
 from rpython.rlib.signature import signature
 from rpython.rlib.unroll import unrolling_iterable
@@ -20,6 +20,7 @@
 from rpython.rtyper.llannotation import lltype_to_annotation
 from rpython.rtyper.annlowlevel import MixLevelHelperAnnotator
 from rpython.tool.ansi_print import AnsiLogger
+from rpython.translator.sandbox.graphchecker import make_abort_graph
 
 log = AnsiLogger("sandbox")
 
@@ -99,34 +100,43 @@
          lltype.typeOf(rpy_sandbox_arg[arg_kind]).TO.ARGS[0])
         for arg_kind in arg_kinds])
 
-    result_func = rpy_sandbox_res[result_kind]
-    RESTYPE = FUNCTYPE.RESULT
+    if fnobj._safe_not_sandboxed == 'abort':
 
-    try:
-        lst = rtyper._sandboxed_functions
-    except AttributeError:
-        lst = rtyper._sandboxed_functions = []
-    name_and_sig = '%s(%s)%s' % (fnname, ''.join(arg_kinds), result_kind)
-    lst.append(name_and_sig)
-    log(name_and_sig)
-    name_and_sig = rffi.str2charp(name_and_sig, track_allocation=False)
+        msg = "sandboxed subprocess aborts on call to %r" % (fnname,)
+        def execute(*args):
+            debug.fatalerror(msg)
 
-    def execute(*args):
-        #
-        # serialize the arguments
-        i = 0
-        for arg_kind, func, ARGTYPE in unroll_args:
-            if arg_kind == 'v':
-                continue
-            func(rffi.cast(ARGTYPE, args[i]))
-            i = i + 1
-        #
-        # send the function name and the arguments and wait for an answer
-        result = result_func(name_and_sig)
-        #
-        # result the answer, if any
-        if RESTYPE is not lltype.Void:
-            return rffi.cast(RESTYPE, result)
+    else:
+
+        result_func = rpy_sandbox_res[result_kind]
+        RESTYPE = FUNCTYPE.RESULT
+
+        try:
+            lst = rtyper._sandboxed_functions
+        except AttributeError:
+            lst = rtyper._sandboxed_functions = []
+        name_and_sig = '%s(%s)%s' % (fnname, ''.join(arg_kinds), result_kind)
+        lst.append(name_and_sig)
+        log(name_and_sig)
+        name_and_sig = rffi.str2charp(name_and_sig, track_allocation=False)
+
+        def execute(*args):
+            #
+            # serialize the arguments
+            i = 0
+            for arg_kind, func, ARGTYPE in unroll_args:
+                if arg_kind == 'v':
+                    continue
+                func(rffi.cast(ARGTYPE, args[i]))
+                i = i + 1
+            #
+            # send the function name and the arguments and wait for an answer
+            result = result_func(name_and_sig)
+            #
+            # result the answer, if any
+            if RESTYPE is not lltype.Void:
+                return rffi.cast(RESTYPE, result)
+    #
     execute.__name__ = 'sandboxed_%s' % (fnname,)
     #
     args_s, s_result = sig_ll(fnobj)
diff --git a/rpython/translator/sandbox/test/test_graphchecker.py b/rpython/translator/sandbox/test/test_graphchecker.py
new file mode 100644
--- /dev/null
+++ b/rpython/translator/sandbox/test/test_graphchecker.py
@@ -0,0 +1,128 @@
+from rpython.translator.translator import TranslationContext, graphof
+from rpython.rtyper.lltypesystem import lltype, rffi
+from rpython.rtyper.lltypesystem.lloperation import llop
+from rpython.rlib.objectmodel import sandbox_review
+
+from rpython.translator.sandbox.graphchecker import GraphChecker
+from rpython.translator.sandbox.graphchecker import make_abort_graph
+
+
+class TestGraphIsUnsafe(object):
+
+    def graph_is_unsafe(self, fn, signature=[]):
+        t = TranslationContext()
+        self.t = t
+        t.buildannotator().build_types(fn, signature)
+        t.buildrtyper().specialize()
+        graph = graphof(t, fn)
+
+        checker = GraphChecker(t)
+        return checker.graph_is_unsafe(graph)
+
+    def check_safe(self, fn, signature=[]):
+        result = self.graph_is_unsafe(fn, signature)
+        assert result is None
+
+    def check_unsafe(self, error_substring, fn, signature=[]):
+        result = self.graph_is_unsafe(fn, signature)
+        assert result is not None
+        assert error_substring in result
+
+    def test_simple(self):
+        def f():
+            pass
+        self.check_safe(f)
+
+    def test_unsafe_setfield(self):
+        S = lltype.Struct('S', ('x', lltype.Signed))
+        s = lltype.malloc(S, flavor='raw', immortal=True)
+        def f():
+            s.x = 42
+        self.check_unsafe("non-GC memory write", f)
+
+    def test_unsafe_operation(self):
+        def f():
+            llop.debug_forked(lltype.Void)
+        self.check_unsafe("unsupported llop", f)
+
+    def test_force_cast(self):
+        SRAW = lltype.Struct('SRAW', ('x', lltype.Signed))
+        SGC = lltype.GcStruct('SGC', ('x', lltype.Signed))
+        def f(x):
+            return llop.force_cast(lltype.Signed, x)
+        self.check_safe(f, [float])
+        self.check_safe(f, [lltype.Ptr(SRAW)])
+        self.check_unsafe("argument is a GC ptr", f, [lltype.Ptr(SGC)])
+
+    def test_direct_call_to_check_caller(self):
+        @sandbox_review(check_caller=True)
+        def g():
+            pass
+        def f():
+            g()
+        self.check_unsafe("direct_call to a graph with check_caller=True", f)
+
+    def test_direct_call_to_reviewed(self):
+        @sandbox_review(reviewed=True)
+        def g():
+            pass
+        def f():
+            g()
+        self.check_safe(f)
+
+    def test_direct_call_to_abort(self):
+        @sandbox_review(abort=True)
+        def g():
+            pass
+        def f():
+            g()
+        self.check_safe(f)
+
+    def test_indirect_call_to_check_caller(self):
+        class A:
+            def meth(self, i):
+                pass
+        class B(A):
+            def meth(self, i):
+                pass
+        class C(A):
+            @sandbox_review(check_caller=True)
+            def meth(self, i):
+                pass
+        def f(i):
+            if i > 5:
+                x = B()
+            else:
+                x = C()
+            x.meth(i)
+        self.check_unsafe("indirect_call that can go to at least one "
+                          "graph with check_caller=True", f, [int])
+
+    def test_direct_call_external(self):
+        llfn1 = rffi.llexternal("foobar", [], lltype.Void, sandboxsafe=True,
+                                _nowrapper=True)
+        self.check_safe(lambda: llfn1())
+        #
+        llfn2 = rffi.llexternal("foobar", [], lltype.Void, sandboxsafe=False,
+                                _nowrapper=True)
+        self.check_safe(lambda: llfn2())   # will be turned into an I/O stub
+        #
+        llfn2b = rffi.llexternal("foobar", [], lltype.Void,
+                                 sandboxsafe="check_caller",
+                                 _nowrapper=True)
+        self.check_unsafe("direct_call to llfunc with "
+                          "sandboxsafe='check_caller'", lambda: llfn2b())
+        #
+        llfn3 = rffi.llexternal("foobar", [], lltype.Void, sandboxsafe=True)
+        self.check_safe(lambda: llfn3())
+        #
+        llfn4 = rffi.llexternal("foobar", [], lltype.Void, sandboxsafe=False)
+        self.check_safe(lambda: llfn4())
+
+    def test_make_abort_graph(self):
+        def dummy():
+            pass
+        self.check_safe(dummy)
+        graph = graphof(self.t, dummy)
+        make_abort_graph(graph)
+        assert graph.startblock.operations[0].opname == 'debug_fatalerror'


More information about the pypy-commit mailing list