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 52def check_rpcs_still_possible(stub) 53 # Expect three more RPCs to succeed. Use a retry loop because the server 54 # thread pool might still be busy processing the previous round of RPCs. 55 3.times do 56 timeout_seconds = 30 57 deadline = Time.now + timeout_seconds 58 success = false 59 while Time.now < deadline 60 STDERR.puts 'now perform another RPC and expect OK...' 61 begin 62 stub.echo(Echo::EchoRequest.new(request: 'hello'), deadline: Time.now + 10) 63 success = true 64 break 65 rescue GRPC::BadStatus => e 66 STDERR.puts "RPC received status: #{e}. Try again..." 67 end 68 end 69 unless success 70 fail "failed to complete a successful RPC within #{timeout_seconds} seconds" 71 end 72 end 73end 74 75# rubocop:disable Metrics/MethodLength 76def main 77 server_runner = ServerRunner.new(EchoServerImpl) 78 server_runner.server_creds = create_server_creds 79 server_port = server_runner.run 80 channel_args = { 81 GRPC::Core::Channel::SSL_TARGET => 'foo.test.google.fr' 82 } 83 token_fetch_attempts = MutableValue.new(0) 84 token_fetch_attempts_mu = Mutex.new 85 jwt_aud_uri_extraction_success_count = MutableValue.new(0) 86 jwt_aud_uri_extraction_success_count_mu = Mutex.new 87 expected_jwt_aud_uri = 'https://foo.test.google.fr/echo.EchoServer' 88 jwt_aud_uri_failure_values = [] 89 times_out_first_time_auth_proc = proc do |args| 90 # We check the value of jwt_aud_uri not necessarily as a test for 91 # the correctness of jwt_aud_uri w.r.t. its expected semantics, but 92 # more for as an indirect way to check for memory corruption. 93 jwt_aud_uri_extraction_success_count_mu.synchronize do 94 if args[:jwt_aud_uri] == expected_jwt_aud_uri 95 jwt_aud_uri_extraction_success_count.value += 1 96 else 97 jwt_aud_uri_failure_values << args[:jwt_aud_uri] 98 end 99 end 100 token_fetch_attempts_mu.synchronize do 101 old_val = token_fetch_attempts.value 102 token_fetch_attempts.value += 1 103 if old_val.zero? 104 STDERR.puts 'call creds plugin sleeping for 4 seconds' 105 sleep 4 106 STDERR.puts 'call creds plugin done with 4 second sleep' 107 raise 'test exception thrown purposely from call creds plugin' 108 end 109 end 110 { 'authorization' => 'fake_val' }.merge(args) 111 end 112 channel_creds = create_channel_creds.compose( 113 GRPC::Core::CallCredentials.new(times_out_first_time_auth_proc)) 114 stub = Echo::EchoServer::Stub.new("localhost:#{server_port}", 115 channel_creds, 116 channel_args: channel_args) 117 STDERR.puts 'perform a first few RPCs to try to get things into a bad state...' 118 threads = [] 119 got_at_least_one_failure = MutableValue.new(false) 120 2000.times do 121 threads << Thread.new do 122 begin 123 # 2 seconds is chosen as deadline here because it is less than the 4 second 124 # sleep that the first call creds user callback does. The idea here is that 125 # a lot of RPCs will be made concurrently all with 2 second deadlines, and they 126 # will all queue up onto the call creds user callback thread, and will all 127 # have to wait for the first 4 second sleep to finish. When the deadlines 128 # of the associated calls fire ~2 seconds in, some of their C-core data 129 # will have ownership dropped, and they will hit the user-after-free in 130 # https://github.com/grpc/grpc/issues/19195 if this isn't handled correctly. 131 stub.echo(Echo::EchoRequest.new(request: 'hello'), deadline: Time.now + 2) 132 rescue GRPC::BadStatus 133 got_at_least_one_failure.value = true 134 # We don't care if these RPCs succeed or fail. The purpose of these 135 # RPCs is just to try to induce a specific use-after-free bug, and to get 136 # the call credentials callback thread into a bad state. 137 end 138 end 139 end 140 threads.each(&:join) 141 unless got_at_least_one_failure.value 142 fail 'expected at least one of the initial RPCs to fail' 143 end 144 check_rpcs_still_possible(stub) 145 jwt_aud_uri_extraction_success_count_mu.synchronize do 146 if jwt_aud_uri_extraction_success_count.value < 4 147 fail "Expected auth metadata plugin callback to be ran with the jwt_aud_uri 148parameter matching its expected value at least 4 times (at least 1 out of the 2000 149initial expected-to-timeout RPCs should have caused this by now, and all three of the 150successful RPCs should have caused this). This test isn't doing what it's meant to do." 151 end 152 unless jwt_aud_uri_failure_values.empty? 153 fail "Expected to get jwt_aud_uri:#{expected_jwt_aud_uri} passed to call creds 154user callback every time that it was invoked, but it did not match the expected value 155in #{jwt_aud_uri_failure_values.size} invocations. This suggests that either: 156a) the expected jwt_aud_uri value is incorrect 157b) there is some corruption of the jwt_aud_uri argument 158Here are are the values of the jwt_aud_uri parameter that were passed to the call 159creds user callback that did not match #{expected_jwt_aud_uri}: 160#{jwt_aud_uri_failure_values}" 161 end 162 end 163 server_runner.stop 164end 165 166main 167