• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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