• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package org.conscrypt;
18 
19 import android.os.Bundle;
20 import android.system.ErrnoException;
21 import android.system.Os;
22 import android.util.Log;
23 import androidx.test.InstrumentationRegistry;
24 import androidx.test.internal.runner.listener.InstrumentationRunListener;
25 import com.android.org.conscrypt.Conscrypt;
26 import java.io.IOException;
27 import java.lang.reflect.InvocationTargetException;
28 import java.lang.reflect.Method;
29 import java.net.Socket;
30 import java.util.Objects;
31 import javax.net.ssl.SSLSocketFactory;
32 import org.junit.runner.Description;
33 import org.junit.runner.Result;
34 
35 /**
36  * An @link(InstrumentationRunListener) which can be used in CTS tests to control the
37  * implementation of @link{SSLSocket} returned by Conscrypt, allowing both implementations
38  * to be tested using the same test classes.
39  *
40  * This listener looks for an instrumentation arg named "conscrypt_sslsocket_implementation".
41  * If its value is "fd" then the file descriptor based implementation will be used, or
42  * if its value is "engine" then the SSLEngine based implementation will be used. Any other
43  * value is invalid.
44  *
45  * The default is set from an @code{testRunStarted} method, i.e. before any tests run and
46  * persists until the ART VM exits, i.e. until all tests in this @code{<test>} clause complete.
47  */
48 public class ConscryptInstrumentationListener extends InstrumentationRunListener {
49     private static final String IMPLEMENTATION_ARG_NAME = "conscrypt_sslsocket_implementation";
50     private static final String LOG_TAG = "ConscryptInstList";
51     // Signal used to trigger a dump of Clang coverage information.
52     // See {@code maybeDumpNativeCoverage} below.
53     private final int COVERAGE_SIGNAL = 37;
54 
55     private enum Implementation {
56         ENGINE(true, "com.android.org.conscrypt.ConscryptEngineSocket"),
57         FD(false, "com.android.org.conscrypt.ConscryptFileDescriptorSocket");
58 
59         private final boolean useEngine;
60         private final String expectedClassName;
61 
Implementation(boolean useEngine, String expectedClassName)62         private Implementation(boolean useEngine, String expectedClassName) {
63             this.useEngine = useEngine;
64             this.expectedClassName = expectedClassName;
65         }
66 
shouldUseEngine()67         private boolean shouldUseEngine() {
68             return useEngine;
69         }
70 
getExpectedClass()71         private Class<? extends Socket> getExpectedClass() {
72             try {
73                 return Class.forName(expectedClassName).asSubclass(Socket.class);
74             } catch (ClassNotFoundException e) {
75                 throw new IllegalStateException(
76                         "Invalid SSLSocket class: '" + expectedClassName + "'");
77             }
78         }
79 
lookup(String name)80         private static Implementation lookup(String name) {
81             try {
82                 return valueOf(name.toUpperCase());
83             } catch (Exception e) {
84                 throw new IllegalArgumentException(
85                         "Invalid SSLSocket implementation: '" + name + "'");
86             }
87         }
88     }
89 
90     @Override
testRunStarted(Description description)91     public void testRunStarted(Description description) throws Exception {
92         Bundle argsBundle = InstrumentationRegistry.getArguments();
93         String implementationName = argsBundle.getString(IMPLEMENTATION_ARG_NAME);
94         Implementation implementation = Implementation.lookup(implementationName);
95         selectImplementation(implementation);
96         super.testRunStarted(description);
97     }
98 
99     @Override
testRunFinished(Result result)100     public void testRunFinished(Result result) throws Exception {
101         maybeDumpNativeCoverage();
102         super.testRunFinished(result);
103     }
104 
105     /**
106      * If this test process is instrumented for native coverage, then trigger a dump
107      * of the coverage data and wait until either we detect the dumping has finished or 60 seconds,
108      * whichever is shorter.
109      *
110      * Background: Coverage builds install a signal handler for signal 37 which flushes coverage
111      * data to disk, which may take a few seconds.  Tests running as an app process will get
112      * killed with SIGKILL once the app code exits, even if the coverage handler is still running.
113      *
114      * Method: If a handler is installed for signal 37, then assume this is a coverage run and
115      * send signal 37.  The handler is non-reentrant and so signal 37 will then be blocked until
116      * the handler completes. So after we send the signal, we loop checking the blocked status
117      * for signal 37 until we hit the 60 second deadline.  If the signal is blocked then sleep for
118      * 2 seconds, and if it becomes unblocked then the handler exitted so we can return early.
119      * If the signal is not blocked at the start of the loop then most likely the handler has
120      * not yet been invoked.  This should almost never happen as it should get blocked on delivery
121      * when we call {@code Os.kill()}, so sleep for a shorter duration (100ms) and try again.  There
122      * is a race condition here where the handler is delayed but then runs for less than 100ms and
123      * gets missed, in which case this method will loop with 100ms sleeps until the deadline.
124      *
125      * In the case where the handler runs for more than 60 seconds, the test process will be allowed
126      * to exit so coverage information may be incomplete.
127      *
128      * There is no API for determining signal dispositions, so this method uses the
129      * {@link SignalMaskInfo} class to read the data from /proc.  If there is an error parsing
130      * the /proc data then this method will also loop until the 60s deadline passes.
131      *
132      */
maybeDumpNativeCoverage()133     private void maybeDumpNativeCoverage() {
134         SignalMaskInfo siginfo = new SignalMaskInfo();
135         if (!siginfo.isValid()) {
136             Log.e(LOG_TAG, "Invalid signal info");
137             return;
138         }
139 
140         if (!siginfo.isCaught(COVERAGE_SIGNAL)) {
141             // Process is not instrumented for coverage
142             Log.i(LOG_TAG, "Not dumping coverage, no handler installed");
143             return;
144         }
145 
146         Log.i(LOG_TAG,
147                 String.format("Sending coverage dump signal %d to pid %d uid %d", COVERAGE_SIGNAL,
148                         Os.getpid(), Os.getuid()));
149         try {
150             Os.kill(Os.getpid(), COVERAGE_SIGNAL);
151         } catch (ErrnoException e) {
152             Log.e(LOG_TAG, "Unable to send coverage signal", e);
153             return;
154         }
155 
156         long start = System.currentTimeMillis();
157         long deadline = start + 60 * 1000L;
158         while (System.currentTimeMillis() < deadline) {
159             siginfo.refresh();
160             try {
161                 if (siginfo.isValid() && siginfo.isBlocked(COVERAGE_SIGNAL)) {
162                     // Signal is currently blocked so assume a handler is running
163                     Thread.sleep(2000L);
164                     siginfo.refresh();
165                     if (siginfo.isValid() && !siginfo.isBlocked(COVERAGE_SIGNAL)) {
166                         // Coverage handler exited while we were asleep
167                         Log.i(LOG_TAG,
168                                 String.format("Coverage dump detected finished after %dms",
169                                         System.currentTimeMillis() - start));
170                         break;
171                     }
172                 } else {
173                     // Coverage signal handler not yet started or invalid siginfo
174                     Thread.sleep(100L);
175                 }
176             } catch (InterruptedException e) {
177                 // ignored
178             }
179         }
180     }
181 
selectImplementation(Implementation implementation)182     private void selectImplementation(Implementation implementation) {
183         // Invoke setUseEngineSocketByDefault by reflection as it is an "ExperimentalApi which is
184         // not visible to tests.
185         try {
186             Method method =
187                     Conscrypt.class.getDeclaredMethod("setUseEngineSocketByDefault", boolean.class);
188             method.invoke(null, implementation.shouldUseEngine());
189         } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
190             throw new IllegalStateException("Unable to set SSLSocket implementation", e);
191         }
192 
193         // Verify that the default socket factory returns the expected implementation class for
194         // SSLSocket or, more likely, a subclass of it.
195         Socket socket;
196         try {
197             socket = SSLSocketFactory.getDefault().createSocket();
198         } catch (IOException e) {
199             throw new IllegalStateException("Unable to create an SSLSocket", e);
200         }
201 
202         Objects.requireNonNull(socket);
203         Class<? extends Socket> expectedClass = implementation.getExpectedClass();
204         if (!expectedClass.isAssignableFrom(socket.getClass())) {
205             throw new IllegalArgumentException("Expected SSLSocket class or subclass of "
206                     + expectedClass.getSimpleName() + " but got "
207                     + socket.getClass().getSimpleName());
208         }
209     }
210 }
211