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