# Copyright 2017 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. require "google/cloud/debugger/breakpoint/source_location" require "google/cloud/debugger/breakpoint/stack_frame" require "google/cloud/debugger/breakpoint/variable" module Google module Cloud module Debugger class Breakpoint ## # Helps to evaluate program state at the location of breakpoint during # executing. The program state, such as local variables and call stack, # are retrieved using Ruby Binding objects. # # The breakpoints may consist of conditional expression and other # code expressions. The Evaluator helps evaluates these expression in # a read-only context. Meaning if the expressions trigger any write # operations in middle of the evaluation, the evaluator is able to # abort the operation and prevent the program state from being altered. # # The evaluated results are saved onto the breakpoints fields. See # [Stackdriver Breakpoints # Doc](https://cloud.google.com/debugger/api/reference/rpc/google.devtools.clouddebugger.v2#google.devtools.clouddebugger.v2.Breakpoint) # for details. # class Evaluator ## # @private YARV bytecode that the evaluator blocks during expression # evaluation. If the breakpoint contains expressions that uses the # following bytecode, the evaluator will block the expression # evaluation from execusion. BYTE_CODE_BLACKLIST = %w[ setinstancevariable setclassvariable setconstant setglobal defineclass opt_ltlt opt_aset opt_aset_with ].freeze ## # @private YARV bytecode that the evaluator blocks during expression # evaluation on the top level. (not from within by predefined methods) LOCAL_BYTE_CODE_BLACKLIST = %w[ setlocal ].freeze ## # @private YARV bytecode call flags that the evaluator blocks during # expression evaluation FUNC_CALL_FLAG_BLACKLIST = %w[ ARGS_BLOCKARG ].freeze ## # @private YARV instructions catch table type that the evaluator # blocks during expression evaluation CATCH_TABLE_TYPE_BLACKLIST = %w[ rescue StandardError ].freeze ## # @private Predefined regex. Saves time during runtime. BYTE_CODE_BLACKLIST_REGEX = /^\d+ #{BYTE_CODE_BLACKLIST.join '|'}/.freeze ## # @private Predefined regex. Saves time during runtime. FULL_BYTE_CODE_BLACKLIST_REGEX = /^\d+ #{ [*BYTE_CODE_BLACKLIST, *LOCAL_BYTE_CODE_BLACKLIST].join '|' }/.freeze ## # @private Predefined regex. Saves time during runtime. FUNC_CALL_FLAG_BLACKLIST_REGEX = / hashify(%I[new]).freeze, Array => hashify(%I[new [] try_convert]).freeze, BasicObject => hashify(%I[new]).freeze, Exception => hashify(%I[exception new]).freeze, Enumerator => hashify(%I[new]).freeze, Fiber => hashify(%I[current]).freeze, FiberError => hashify(%I[new]).freeze, File => hashify( %I[ basename dirname extname join path split ] ).freeze, FloatDomainError => hashify(%I[new]).freeze, Hash => hashify(%I[new [] try_convert]).freeze, IndexError => hashify(%I[new]).freeze, KeyError => hashify(%I[new]).freeze, Module => hashify(%I[constants nesting used_modules]).freeze, NameError => hashify(%I[new]).freeze, NoMethodError => hashify(%I[new]).freeze, Object => hashify(%I[new]).freeze, RangeError => hashify(%I[new]).freeze, RegexpError => hashify(%I[new]).freeze, RuntimeError => hashify(%I[new]).freeze, String => hashify(%I[new try_convert]).freeze, Thread => hashify( %I[ DEBUG abort_on_exception current list main pending_interrupt? report_on_exception ] ).freeze, Time => hashify( %I[ at gm local mktime new now utc ] ).freeze, TypeError => hashify(%I[new]).freeze, Google::Cloud::Debugger::Breakpoint::Evaluator => hashify( %I[disable_method_trace_for_thread] ).freeze, ZeroDivisionError => hashify(%I[new]).freeze }.freeze ## # @private List of C level instance methods that the evaluator allows # during expression evaluation C_INSTANCE_METHOD_WHITELIST = { ArgumentError => hashify(%I[initialize]).freeze, Array => hashify( %I[ initialize & * + - <=> == any? assoc at bsearch bsearch_index collect combination compact [] count cycle dig drop drop_while each each_index empty? eql? fetch find_index first flatten frozen? hash include? index inspect to_s join last length map max min pack permutation product rassoc reject repeated_combination repeated_permutation reverse reverse_each rindex rotate sample select shuffle size slice sort sum take take_while to_a to_ary to_h transpose uniq values_at zip | ] ).freeze, BasicObject => hashify( %I[ initialize ! != == __id__ method_missing object_id send __send__ equal? ] ).freeze, Binding => hashify( %I[ local_variable_defined? local_variable_get local_variables receiver ] ).freeze, Class => hashify(%I[superclass]).freeze, Dir => hashify(%I[inspect path to_path]).freeze, Exception => hashify( %I[ initialize == backtrace backtrace_locations cause exception inspect message to_s ] ).freeze, Enumerator => hashify( %I[ initialize each each_with_index each_with_object inspect size with_index with_object ] ).freeze, Fiber => hashify(%I[alive?]).freeze, FiberError => hashify(%I[initialize]).freeze, File => hashify(%I[path to_path]).freeze, FloatDomainError => hashify(%I[initialize]).freeze, Hash => hashify( %I[ initialize < <= == > >= [] any? assoc compact compare_by_identity? default_proc dig each each_key each_pair each_value empty? eql? fetch fetch_values flatten has_key? has_value? hash include? to_s inspect invert key key? keys length member? merge rassoc reject select size to_a to_h to_hash to_proc transform_values value? values value_at ] ).freeze, IndexError => hashify(%I[initialize]).freeze, IO => hashify( %I[ autoclose? binmode? close_on_exec? closed? encoding inspect internal_encoding sync ] ).freeze, KeyError => hashify(%I[initialize]).freeze, Method => hashify( %I[ == [] arity call clone curry eql? hash inspect name original_name owner parameters receiver source_location super_method to_proc to_s ] ).freeze, Module => hashify( %I[ < <= <=> == === > >= ancestors autoload? class_variable_defined? class_variable_get class_variables const_defined? const_get constants include? included_modules inspect instance_method instance_methods method_defined? name private_instance_methods private_method_defined? protected_instance_methods protected_method_defined? public_instance_method public_instance_methods public_method_defined? singleton_class? to_s ] ).freeze, Mutex => hashify(%I[locked? owned?]).freeze, NameError => hashify(%I[initialize]).freeze, NoMethodError => hashify(%I[initialize]).freeze, RangeError => hashify(%I[initialize]).freeze, RegexpError => hashify(%I[initialize]).freeze, RuntimeError => hashify(%I[initialize]).freeze, String => hashify( %I[ initialize % * + +@ -@ <=> == === =~ [] ascii_only? b bytes bytesize byteslice capitalize casecmp casecmp? center chars chomp chop chr codepoints count crypt delete downcase dump each_byte each_char each_codepoint each_line empty? encoding end_with? eql? getbyte gsub hash hex include? index inspect intern length lines ljust lstrip match match? next oct ord partition reverse rindex rjust rpartition rstrip scan scrub size slice split squeeze start_with? strip sub succ sum swapcase to_c to_f to_i to_r to_s to_str to_sym tr tr_s unpack unpack1 upcase upto valid_encoding? ] ).freeze, ThreadGroup => hashify(%I[enclosed? list]).freeze, Thread => hashify( %I[ [] abort_on_exception alive? backtrace backtrace_locations group inspect key? keys name pending_interrupt? priority report_on_exception safe_level status stop? thread_variable? thread_variable_get thread_variables ] ).freeze, Time => hashify( %I[ initialize + - <=> asctime ctime day dst? eql? friday? getgm getlocal getuc gmt gmt_offset gmtoff hash hour inspect isdst mday min mon month monday? month nsec round saturday? sec strftime subsec succ sunday? thursday? to_a to_f to_i to_r to_s tuesday? tv_nsec tv_sec tv_usec usec utc? utc_offset wday wednesday? yday year zone ] ).freeze, TypeError => hashify(%I[initialize]).freeze, UnboundMethod => hashify( %I[ == arity clone eql? hash inspect name original_name owner parameters source_location super_method to_s ] ).freeze, ZeroDivisionError => hashify(%I[initialize]).freeze, # Modules Kernel => hashify( %I[ Array Complex Float Hash Integer Rational String __callee__ __dir__ __method__ autoload? block_given? caller caller_locations catch format global_variables iterator? lambda local_variables loop method methods proc rand !~ <=> === =~ class clone dup enum_for eql? frozen? hash inspect instance_of? instance_variable_defined? instance_variable_get instance_variables is_a? itself kind_of? nil? object_id private_methods protected_methods public_method public_methods public_send respond_to? respond_to_missing? __send__ send singleton_class singleton_method singleton_methods tainted? tap to_enum to_s untrusted? ] ).freeze }.freeze ## # @private List of Ruby class methods that the evaluator allows # during expression evaluation RUBY_CLASS_METHOD_WHITELIST = { # Classes Debugger => hashify(%I[allow_mutating_methods!]).freeze }.freeze ## # @private List of Ruby instance methods that the evaluator allows # during expression evaluation RUBY_INSTANCE_METHOD_WHITELIST = { Evaluator => hashify(%I[allow_mutating_methods!]).freeze }.freeze PROHIBITED_OPERATION_MSG = "Prohibited operation detected".freeze MUTATION_DETECTED_MSG = "Mutation detected!".freeze COMPILATION_FAIL_MSG = "Unable to compile expression".freeze LONG_EVAL_MSG = "Evaluation exceeded time limit".freeze EVALUATOR_REFERENCE = :__evaluator__ ## # @private # Read-only evaluates a single expression in a given # context binding. Handles any exceptions raised. # # @param [Binding] binding The binding object from the context # @param [String] expression A string of code to be evaluated # # @return [Object] The result Ruby object from evaluating the # expression. If the expression is blocked from mutating # the state of program. A # {Google::Cloud::Debugger::MutationError} will be returned. # def self.readonly_eval_expression binding, expression new.readonly_eval_expression binding, expression end ## # @private # Returns the evaluator currently running, or nil if an evaluation # is not currently running. # # @return [Evaluator, nil] # def self.current Thread.current.thread_variable_get EVALUATOR_REFERENCE end ## # Create a new Evaluator. # @private # # @param [boolean, nil] allow_mutating_methods whether to allow # calling of potentially mutating methods, or nil to default to # the current configuration setting. # @param [Numeric, nil] time_limit the time limit in seconds, or nil # to default to the current configuration setting. # def initialize allow_mutating_methods: nil, time_limit: nil @allow_mutating_methods = if allow_mutating_methods.nil? Debugger.configure.allow_mutating_methods else allow_mutating_methods end @time_limit = time_limit || Debugger.configure.evaluation_time_limit end ## # Allow calling of mutating methods even if mutation detection is # configured to be active. This may be called only during debugger # condition or expression evaluation. # # If you pass in a block, it will be evaluated with mutation # detection disabled, and the original setting will be restored # afterward. If you do not pass a block, this evaluator will # permanently allow mutating methods. # @private # def allow_mutating_methods! old = @allow_mutating_methods @allow_mutating_methods = true return unless block_given? result = yield @allow_mutating_methods = old result end ## # Read-only evaluates a single expression in a given # context binding. Handles any exceptions raised. # @private # # @param [Binding] binding The binding object from the context # @param [String] expression A string of code to be evaluates # # @return [Object] The result Ruby object from evaluating the # expression. If the expression is blocked from mutating # the state of program. A # {Google::Cloud::Debugger::MutationError} will be returned. # def readonly_eval_expression binding, expression compilation_result = validate_compiled_expression expression return compilation_result if compilation_result.is_a? Exception readonly_eval_expression_exec binding, expression rescue StandardError => e "Unable to evaluate expression: #{e.message}" end private ## # @private Actually read-only evaluates an expression in a given # context binding. The evaluation is done in a separate thread due # to this method may be run from Ruby Trace call back, where # addtional code tracing is disabled in original thread. # # @param [Binding] binding The binding object from the context # @param [String] expression A string of code to be evaluates # # @return [Object] The result Ruby object from evaluating the # expression. It returns Google::Cloud::Debugger::MutationError # if a mutation is caught. # def readonly_eval_expression_exec binding, expression # The evaluation is most likely triggered from a trace callback, # where addtional nested tracing is disabled by VM. So we need to # do evaluation in a new thread, where function calls can be # traced. thr = Thread.new do Thread.current.thread_variable_set EVALUATOR_REFERENCE, self binding.eval wrap_expression(expression) rescue StandardError, EvaluationError => e # Treat all StandardError as mutation and set @mutation_cause unless e.instance_variable_get :@mutation_cause e.instance_variable_set( :@mutation_cause, Google::Cloud::Debugger::EvaluationError::UNKNOWN_CAUSE ) end e end thr.join @time_limit # Force terminate evaluation thread if not finished already and # return an Exception if thr.alive? thr.kill Google::Cloud::Debugger::EvaluationError.new LONG_EVAL_MSG else thr.value end end ## # @private Compile the expression into YARV instructions. Return # Google::Cloud::Debugger::MutationError if any prohibited YARV # instructions are found. # # @param [String] expression String of code expression # # @return [String,Google::Cloud::Debugger::MutationError] It returns # the compile YARV instructions if no prohibited bytecodes are # found. Otherwise return Google::Cloud::Debugger::MutationError. # def validate_compiled_expression expression begin yarv_instructions = RubyVM::InstructionSequence.compile(expression).disasm rescue ScriptError return Google::Cloud::Debugger::MutationError.new( COMPILATION_FAIL_MSG, Google::Cloud::Debugger::EvaluationError::PROHIBITED_YARV ) end unless immutable_yarv_instructions? yarv_instructions return Google::Cloud::Debugger::MutationError.new( MUTATION_DETECTED_MSG, Google::Cloud::Debugger::EvaluationError::PROHIBITED_YARV ) end yarv_instructions end ## # @private Helps checking if a given set of YARV instructions # contains any prohibited bytecode or instructions. # # @param [String] yarv_instructions Compiled YARV instructions # string # @param [Boolean] allow_localops Whether allows local variable # write operations # # @return [Boolean] True if the YARV instructions don't contain any # prohibited operations. Otherwise false. # def immutable_yarv_instructions? yarv_instructions, allow_localops: false byte_code_blacklist_regex = if allow_localops BYTE_CODE_BLACKLIST_REGEX else FULL_BYTE_CODE_BLACKLIST_REGEX end func_call_flag_blacklist_regex = FUNC_CALL_FLAG_BLACKLIST_REGEX catch_table_type_blacklist_regex = CATCH_TABLE_BLACKLIST_REGEX !(yarv_instructions.match(func_call_flag_blacklist_regex) || yarv_instructions.match(byte_code_blacklist_regex) || yarv_instructions.match(catch_table_type_blacklist_regex)) end ## # @private Wraps expression with tracing code def wrap_expression expression <<~CODE begin ::Google::Cloud::Debugger::Breakpoint::Evaluator.send( :enable_method_trace_for_thread) #{expression} ensure ::Google::Cloud::Debugger::Breakpoint::Evaluator.send( :disable_method_trace_for_thread) end CODE end ## # @private Evaluation tracing callback function. This is called # everytime a Ruby function is called during evaluation of # an expression. # # @param [Object] receiver The receiver of the function being called # @param [Symbol] mid The method name # # @return [NilClass] Nil if no prohibited operations are found. # Otherwise raise Google::Cloud::Debugger::MutationError error. # def trace_func_callback receiver, defined_class, mid return if @allow_mutating_methods if receiver.is_a?(Class) || receiver.is_a?(Module) if whitelisted_ruby_class_method? defined_class, receiver, mid return end elsif whitelisted_ruby_instance_method? defined_class, mid return end yarv_instructions = begin RubyVM::InstructionSequence.disasm receiver.method mid rescue StandardError raise Google::Cloud::Debugger::EvaluationError.new( PROHIBITED_OPERATION_MSG, Google::Cloud::Debugger::EvaluationError::META_PROGRAMMING ) end return if immutable_yarv_instructions?(yarv_instructions, allow_localops: true) raise Google::Cloud::Debugger::MutationError.new( MUTATION_DETECTED_MSG, Google::Cloud::Debugger::EvaluationError::PROHIBITED_YARV ) end ## # @private Evaluation tracing callback function. This is called # everytime a C function is called during evaluation of # an expression. # # @param [Object] receiver The receiver of the function being called # @param [Class] defined_class The Class of where the function is # defined # @param [Symbol] mid The method name # # @return [NilClass] Nil if no prohibited operations are found. # Otherwise raise Google::Cloud::Debugger::MutationError error. # def trace_c_func_callback receiver, defined_class, mid return if @allow_mutating_methods invalid_op = if receiver.is_a?(Class) || receiver.is_a?(Module) !validate_c_class_method(defined_class, receiver, mid) else !validate_c_instance_method(defined_class, mid) end return unless invalid_op Google::Cloud::Debugger::Breakpoint::Evaluator.send( :disable_method_trace_for_thread ) raise Google::Cloud::Debugger::MutationError.new( PROHIBITED_OPERATION_MSG, Google::Cloud::Debugger::EvaluationError::PROHIBITED_C_FUNC ) end ## # @private Helper method to verify whether a C level class method # is allowed or not. def validate_c_class_method klass, receiver, mid IMMUTABLE_CLASSES.include?(receiver) || (C_CLASS_METHOD_WHITELIST[receiver] || {})[mid] || (C_INSTANCE_METHOD_WHITELIST[klass] || {})[mid] end ## # @private Helper method to verify whether a C level instance method # is allowed or not. def validate_c_instance_method klass, mid IMMUTABLE_CLASSES.include?(klass) || (C_INSTANCE_METHOD_WHITELIST[klass] || {})[mid] end ## # @private Helper method to verify whether a ruby class method # is allowed or not. def whitelisted_ruby_class_method? klass, receiver, mid IMMUTABLE_CLASSES.include?(klass) || (RUBY_CLASS_METHOD_WHITELIST[receiver] || {})[mid] || (RUBY_INSTANCE_METHOD_WHITELIST[klass] || {})[mid] end ## # @private Helper method to verify whether a ruby instance method # is allowed or not. def whitelisted_ruby_instance_method? klass, mid IMMUTABLE_CLASSES.include?(klass) || (RUBY_INSTANCE_METHOD_WHITELIST[klass] || {})[mid] end end end ## # @private Custom error type used to identify evaluation error during # breakpoint expression evaluation. class EvaluationError < Exception UNKNOWN_CAUSE = :unknown_cause PROHIBITED_YARV = :prohibited_yarv PROHIBITED_C_FUNC = :prohibited_c_func META_PROGRAMMING = :meta_programming attr_reader :mutation_cause def initialize msg = nil, mutation_cause = UNKNOWN_CAUSE @mutation_cause = mutation_cause super msg end def inspect "#<#{self.class}: #{message}>" end end ## # @private Custom error type used to identify mutation during breakpoint # expression evaluations class MutationError < EvaluationError def initialize msg = "Mutation detected!", *args super end end end end end