#!/usr/bin/env ruby # # Copyright 2016 gRPC authors. # # 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 # # http://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. this_dir = File.expand_path(File.dirname(__FILE__)) protos_lib_dir = File.join(this_dir, 'lib') grpc_lib_dir = File.join(File.dirname(this_dir), 'lib') $LOAD_PATH.unshift(grpc_lib_dir) unless $LOAD_PATH.include?(grpc_lib_dir) $LOAD_PATH.unshift(protos_lib_dir) unless $LOAD_PATH.include?(protos_lib_dir) $LOAD_PATH.unshift(this_dir) unless $LOAD_PATH.include?(this_dir) require 'grpc' require 'end2end_common' def create_channel_creds test_root = File.join(File.dirname(__FILE__), '..', 'spec', 'testdata') files = ['ca.pem', 'client.key', 'client.pem'] creds = files.map { |f| File.open(File.join(test_root, f)).read } GRPC::Core::ChannelCredentials.new(creds[0], creds[1], creds[2]) end def client_cert test_root = File.join(File.dirname(__FILE__), '..', 'spec', 'testdata') cert = File.open(File.join(test_root, 'client.pem')).read fail unless cert.is_a?(String) cert end def create_server_creds test_root = File.join(File.dirname(__FILE__), '..', 'spec', 'testdata') GRPC.logger.info("test root: #{test_root}") files = ['ca.pem', 'server1.key', 'server1.pem'] creds = files.map { |f| File.open(File.join(test_root, f)).read } GRPC::Core::ServerCredentials.new( creds[0], [{ private_key: creds[1], cert_chain: creds[2] }], true) # force client auth end # rubocop:disable Metrics/AbcSize # rubocop:disable Metrics/MethodLength def main server_runner = ServerRunner.new(EchoServerImpl) server_runner.server_creds = create_server_creds server_port = server_runner.run channel_args = { GRPC::Core::Channel::SSL_TARGET => 'foo.test.google.fr' } token_fetch_attempts = MutableValue.new(0) token_fetch_attempts_mu = Mutex.new jwt_aud_uri_extraction_success_count = MutableValue.new(0) jwt_aud_uri_extraction_success_count_mu = Mutex.new expected_jwt_aud_uri = 'https://foo.test.google.fr/echo.EchoServer' jwt_aud_uri_failure_values = [] times_out_first_time_auth_proc = proc do |args| # We check the value of jwt_aud_uri not necessarily as a test for # the correctness of jwt_aud_uri w.r.t. its expected semantics, but # more for as an indirect way to check for memory corruption. jwt_aud_uri_extraction_success_count_mu.synchronize do if args[:jwt_aud_uri] == expected_jwt_aud_uri jwt_aud_uri_extraction_success_count.value += 1 else jwt_aud_uri_failure_values << args[:jwt_aud_uri] end end token_fetch_attempts_mu.synchronize do old_val = token_fetch_attempts.value token_fetch_attempts.value += 1 if old_val.zero? STDERR.puts 'call creds plugin sleeping for 4 seconds' sleep 4 STDERR.puts 'call creds plugin done with 4 second sleep' raise 'test exception thrown purposely from call creds plugin' end end { 'authorization' => 'fake_val' }.merge(args) end channel_creds = create_channel_creds.compose( GRPC::Core::CallCredentials.new(times_out_first_time_auth_proc)) stub = Echo::EchoServer::Stub.new("localhost:#{server_port}", channel_creds, channel_args: channel_args) STDERR.puts 'perform a first few RPCs to try to get things into a bad state...' threads = [] got_at_least_one_failure = MutableValue.new(false) 2000.times do threads << Thread.new do begin # 2 seconds is chosen as deadline here because it is less than the 4 second # sleep that the first call creds user callback does. The idea here is that # a lot of RPCs will be made concurrently all with 2 second deadlines, and they # will all queue up onto the call creds user callback thread, and will all # have to wait for the first 4 second sleep to finish. When the deadlines # of the associated calls fire ~2 seconds in, some of their C-core data # will have ownership dropped, and they will hit the user-after-free in # https://github.com/grpc/grpc/issues/19195 if this isn't handled correctly. stub.echo(Echo::EchoRequest.new(request: 'hello'), deadline: Time.now + 2) rescue GRPC::BadStatus got_at_least_one_failure.value = true # We don't care if these RPCs succeed or fail. The purpose of these # RPCs is just to try to induce a specific use-after-free bug, and to get # the call credentials callback thread into a bad state. end end end threads.each(&:join) unless got_at_least_one_failure.value fail 'expected at least one of the initial RPCs to fail' end # Expect three more RPCs to succeed STDERR.puts 'now perform another RPC and expect OK...' stub.echo(Echo::EchoRequest.new(request: 'hello'), deadline: Time.now + 10) STDERR.puts 'now perform another RPC and expect OK...' stub.echo(Echo::EchoRequest.new(request: 'hello'), deadline: Time.now + 10) STDERR.puts 'now perform another RPC and expect OK...' stub.echo(Echo::EchoRequest.new(request: 'hello'), deadline: Time.now + 10) jwt_aud_uri_extraction_success_count_mu.synchronize do if jwt_aud_uri_extraction_success_count.value < 4 fail "Expected auth metadata plugin callback to be ran with the jwt_aud_uri parameter matching its expected value at least 4 times (at least 1 out of the 2000 initial expected-to-timeout RPCs should have caused this by now, and all three of the successful RPCs should have caused this). This test isn't doing what it's meant to do." end unless jwt_aud_uri_failure_values.empty? fail "Expected to get jwt_aud_uri:#{expected_jwt_aud_uri} passed to call creds user callback every time that it was invoked, but it did not match the expected value in #{jwt_aud_uri_failure_values.size} invocations. This suggests that either: a) the expected jwt_aud_uri value is incorrect b) there is some corruption of the jwt_aud_uri argument Here are are the values of the jwt_aud_uri parameter that were passed to the call creds user callback that did not match #{expected_jwt_aud_uri}: #{jwt_aud_uri_failure_values}" end end server_runner.stop end main