# Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true cs__scoped_require 'monitor' cs__scoped_require 'fiber' cs__scoped_require 'contrast/agent/scope' # This is the Scope component. # # It tracks /Contrast/ scope. That is, "are we currently doing assess # or protect stuff within a patched method?" -- this is how we avoid doing # Contrast stuff on Contrast code. # # Separately from this component, there is also require scope, which is an # optimization on how we implement patching to `require`. module Contrast module Components module Scope # :nodoc: MONITOR = Monitor.new EXECUTION_CONTEXT = {} # rubocop:disable Style/MutableConstant class Interface # :nodoc: include Contrast::Components::ComponentBase def initialize # This is probably redundant with #scope_for_current_ec's nil check. EXECUTION_CONTEXT[Fiber.current] = Contrast::Agent::Scope.new end # This returns the scope governing the current execution context. # Use this sparingly, preferring the instance & class methods to # access and query scope, rather than interacting with the scope # object directly. def scope_for_current_ec MONITOR.synchronize do return EXECUTION_CONTEXT[Fiber.current] ||= Contrast::Agent::Scope.new end end end module InstanceMethods # :nodoc: # For each instance method on a scope, define a forwarder # to the scope on the current execution context's scope. Contrast::Agent::Scope.public_instance_methods(false).each do |method_sym| define_method(method_sym) do |*args, &block| scope_for_current_ec.send(method_sym, *args, &block) end end def scope_for_current_ec MONITOR.synchronize do return EXECUTION_CONTEXT[Fiber.current] ||= Contrast::Agent::Scope.new end end # TODO: https://contrast.atlassian.net/browse/RUBY-290 # # Current behavior is to no-op if we're not "in a request context". # Our C functions were previously checking to see if we had a scope, because # scope was tacked on to a request context -- so "we have a scope, therefore, # we have a request context." We've decoupled scopes from request contexts, # so now it checks "do we have a request context." # RUBY-290 should remove all of that, including this method. def in_request_context? !!Contrast::Agent::REQUEST_TRACKER.current end end def self.sweep_dead_ecs # TODO: RUBY-571, #sweep_dead_ecs compensates for a lack of weak tables # 'ec' for execution context. in this case, it's a Fiber. # Threads rely on Fibers, so two birds, one stone. MONITOR.synchronize do EXECUTION_CONTEXT.delete_if do |ec, _scope| !ec.alive? end end end ClassMethods = InstanceMethods COMPONENT_INTERFACE = Interface.new end end end # This is a reasonable place for the Kernel#catch hook to live. # No current plans for component re-design, but if we had some kind of # "do this when a component is hooked in" thing, this would live there. # For now, it's over-engineering to live anywhere else. -ajm module Kernel # :nodoc: alias_method :cs__catch, :catch # In the event of a `throw`, we need to override `catch` # to save & restore scope state: # # scope_level == 0 # # catch(:abc) do # with_contrast_scope do # throw :abc # will leak # end # end # # scope_level == 1 # # Frankly, this isn't how scope should be used. This is in place of # proper `ensure` blocks within the instrumentation call stack. # This will actually /create/ scope leaks if you're doing something like: # # catch(:ohno) do # enter scope # end # # abc() # # exit scope # # i.e. if you intend to change net scope across a catch block boundary. private def catch *args, &block # Save current scope level scope_level = Contrast::Components::Scope::COMPONENT_INTERFACE.scope_for_current_ec.instance_variable_get(:@contrast_scope) # Run original catch with block. retval = cs__catch(*args, &block) # Restore scope. Contrast::Components::Scope::COMPONENT_INTERFACE.scope_for_current_ec.instance_variable_set(:@contrast_scope, scope_level) retval end end