1#!/usr/bin/env ruby 2# 3# Copyright 2016 gRPC authors. 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17this_dir = File.expand_path(File.dirname(__FILE__)) 18protos_lib_dir = File.join(this_dir, 'lib') 19grpc_lib_dir = File.join(File.dirname(this_dir), 'lib') 20$LOAD_PATH.unshift(grpc_lib_dir) unless $LOAD_PATH.include?(grpc_lib_dir) 21$LOAD_PATH.unshift(protos_lib_dir) unless $LOAD_PATH.include?(protos_lib_dir) 22$LOAD_PATH.unshift(this_dir) unless $LOAD_PATH.include?(this_dir) 23 24require 'grpc' 25require 'end2end_common' 26 27def create_channel_creds 28 test_root = File.join(File.dirname(__FILE__), '..', 'spec', 'testdata') 29 files = ['ca.pem', 'client.key', 'client.pem'] 30 creds = files.map { |f| File.open(File.join(test_root, f)).read } 31 GRPC::Core::ChannelCredentials.new(creds[0], creds[1], creds[2]) 32end 33 34def client_cert 35 test_root = File.join(File.dirname(__FILE__), '..', 'spec', 'testdata') 36 cert = File.open(File.join(test_root, 'client.pem')).read 37 fail unless cert.is_a?(String) 38 cert 39end 40 41def create_server_creds 42 test_root = File.join(File.dirname(__FILE__), '..', 'spec', 'testdata') 43 GRPC.logger.info("test root: #{test_root}") 44 files = ['ca.pem', 'server1.key', 'server1.pem'] 45 creds = files.map { |f| File.open(File.join(test_root, f)).read } 46 GRPC::Core::ServerCredentials.new( 47 creds[0], 48 [{ private_key: creds[1], cert_chain: creds[2] }], 49 true) # force client auth 50end 51 52# rubocop:disable Metrics/AbcSize 53# rubocop:disable Metrics/MethodLength 54def main 55 server_runner = ServerRunner.new(EchoServerImpl) 56 server_runner.server_creds = create_server_creds 57 server_port = server_runner.run 58 channel_args = { 59 GRPC::Core::Channel::SSL_TARGET => 'foo.test.google.fr' 60 } 61 token_fetch_attempts = MutableValue.new(0) 62 token_fetch_attempts_mu = Mutex.new 63 jwt_aud_uri_extraction_success_count = MutableValue.new(0) 64 jwt_aud_uri_extraction_success_count_mu = Mutex.new 65 expected_jwt_aud_uri = 'https://foo.test.google.fr/echo.EchoServer' 66 jwt_aud_uri_failure_values = [] 67 times_out_first_time_auth_proc = proc do |args| 68 # We check the value of jwt_aud_uri not necessarily as a test for 69 # the correctness of jwt_aud_uri w.r.t. its expected semantics, but 70 # more for as an indirect way to check for memory corruption. 71 jwt_aud_uri_extraction_success_count_mu.synchronize do 72 if args[:jwt_aud_uri] == expected_jwt_aud_uri 73 jwt_aud_uri_extraction_success_count.value += 1 74 else 75 jwt_aud_uri_failure_values << args[:jwt_aud_uri] 76 end 77 end 78 token_fetch_attempts_mu.synchronize do 79 old_val = token_fetch_attempts.value 80 token_fetch_attempts.value += 1 81 if old_val.zero? 82 STDERR.puts 'call creds plugin sleeping for 4 seconds' 83 sleep 4 84 STDERR.puts 'call creds plugin done with 4 second sleep' 85 raise 'test exception thrown purposely from call creds plugin' 86 end 87 end 88 { 'authorization' => 'fake_val' }.merge(args) 89 end 90 channel_creds = create_channel_creds.compose( 91 GRPC::Core::CallCredentials.new(times_out_first_time_auth_proc)) 92 stub = Echo::EchoServer::Stub.new("localhost:#{server_port}", 93 channel_creds, 94 channel_args: channel_args) 95 STDERR.puts 'perform a first few RPCs to try to get things into a bad state...' 96 threads = [] 97 got_at_least_one_failure = MutableValue.new(false) 98 2000.times do 99 threads << Thread.new do 100 begin 101 # 2 seconds is chosen as deadline here because it is less than the 4 second 102 # sleep that the first call creds user callback does. The idea here is that 103 # a lot of RPCs will be made concurrently all with 2 second deadlines, and they 104 # will all queue up onto the call creds user callback thread, and will all 105 # have to wait for the first 4 second sleep to finish. When the deadlines 106 # of the associated calls fire ~2 seconds in, some of their C-core data 107 # will have ownership dropped, and they will hit the user-after-free in 108 # https://github.com/grpc/grpc/issues/19195 if this isn't handled correctly. 109 stub.echo(Echo::EchoRequest.new(request: 'hello'), deadline: Time.now + 2) 110 rescue GRPC::BadStatus 111 got_at_least_one_failure.value = true 112 # We don't care if these RPCs succeed or fail. The purpose of these 113 # RPCs is just to try to induce a specific use-after-free bug, and to get 114 # the call credentials callback thread into a bad state. 115 end 116 end 117 end 118 threads.each(&:join) 119 unless got_at_least_one_failure.value 120 fail 'expected at least one of the initial RPCs to fail' 121 end 122 # Expect three more RPCs to succeed 123 STDERR.puts 'now perform another RPC and expect OK...' 124 stub.echo(Echo::EchoRequest.new(request: 'hello'), deadline: Time.now + 10) 125 STDERR.puts 'now perform another RPC and expect OK...' 126 stub.echo(Echo::EchoRequest.new(request: 'hello'), deadline: Time.now + 10) 127 STDERR.puts 'now perform another RPC and expect OK...' 128 stub.echo(Echo::EchoRequest.new(request: 'hello'), deadline: Time.now + 10) 129 jwt_aud_uri_extraction_success_count_mu.synchronize do 130 if jwt_aud_uri_extraction_success_count.value < 4 131 fail "Expected auth metadata plugin callback to be ran with the jwt_aud_uri 132parameter matching its expected value at least 4 times (at least 1 out of the 2000 133initial expected-to-timeout RPCs should have caused this by now, and all three of the 134successful RPCs should have caused this). This test isn't doing what it's meant to do." 135 end 136 unless jwt_aud_uri_failure_values.empty? 137 fail "Expected to get jwt_aud_uri:#{expected_jwt_aud_uri} passed to call creds 138user callback every time that it was invoked, but it did not match the expected value 139in #{jwt_aud_uri_failure_values.size} invocations. This suggests that either: 140a) the expected jwt_aud_uri value is incorrect 141b) there is some corruption of the jwt_aud_uri argument 142Here are are the values of the jwt_aud_uri parameter that were passed to the call 143creds user callback that did not match #{expected_jwt_aud_uri}: 144#{jwt_aud_uri_failure_values}" 145 end 146 end 147 server_runner.stop 148end 149 150main 151