1 /*
2  * Copyright (C) 2017 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 android.seccomp.cts.app;
18 
19 import android.content.ComponentName;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.ServiceConnection;
23 import android.content.res.AssetManager;
24 import android.os.ConditionVariable;
25 import android.os.Handler;
26 import android.os.HandlerThread;
27 import android.os.IBinder;
28 import android.os.Looper;
29 import android.os.Message;
30 import android.os.Messenger;
31 import android.os.RemoteException;
32 import android.util.Log;
33 
34 import androidx.test.InstrumentationRegistry;
35 import androidx.test.runner.AndroidJUnit4;
36 
37 import com.android.compatibility.common.util.CpuFeatures;
38 
39 import org.json.JSONException;
40 import org.json.JSONObject;
41 import org.junit.Assert;
42 import org.junit.Before;
43 import org.junit.Test;
44 import org.junit.runner.RunWith;
45 
46 import java.io.IOException;
47 import java.io.InputStream;
48 import java.util.Iterator;
49 
50 /**
51  * Device-side tests for CtsSeccompHostTestCases
52  */
53 @RunWith(AndroidJUnit4.class)
54 public class SeccompDeviceTest {
55     static {
56         System.loadLibrary("ctsseccomp_jni");
57     }
58 
59     static final String TAG = "SeccompDeviceTest";
60 
61     protected Context mContext;
62 
63     // This is flagged whenever we have obtained the test result from the isolated
64     // service; it allows us to block the test until the results are in.
65     private final ConditionVariable mResultCondition = new ConditionVariable();
66 
67     private boolean mAppZygoteResult;
68     private Messenger mMessenger;
69     private HandlerThread mHandlerThread;
70 
71     // The service start can take a long time, because seccomp denials will
72     // cause process crashes and dumps, which we waitpid() for sequentially.
73     private static final int SERVICE_START_TIMEOUT_MS = 120000;
74 
75     private JSONObject mAllowedSyscallMap;
76     private JSONObject mBlockedSyscallMap;
77 
78     @Before
initializeSyscallMap()79     public void initializeSyscallMap() throws IOException, JSONException {
80         mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
81         AssetManager manager = mContext.getAssets();
82         try (InputStream is = manager.open("syscalls_allowed.json")) {
83             mAllowedSyscallMap = new JSONObject(readInputStreamFully(is));
84         }
85         try (InputStream is = manager.open("syscalls_blocked.json")) {
86             mBlockedSyscallMap = new JSONObject(readInputStreamFully(is));
87         }
88         mHandlerThread = new HandlerThread("HandlerThread");
89         mHandlerThread.start();
90         Looper looper = mHandlerThread.getLooper();
91         mMessenger = new Messenger(new IncomingHandler(looper));
92     }
93 
94     @Test
testCTSSyscallAllowed()95     public void testCTSSyscallAllowed() throws JSONException {
96         JSONObject map = mAllowedSyscallMap.getJSONObject(getCurrentArch());
97         Iterator<String> iter = map.keys();
98         while (iter.hasNext()) {
99             String syscallName = iter.next();
100             testAllowed(map.getInt(syscallName));
101         }
102     }
103 
104     @Test
testCTSSyscallBlocked()105     public void testCTSSyscallBlocked() throws JSONException {
106         JSONObject map = mBlockedSyscallMap.getJSONObject(getCurrentArch());
107         Iterator<String> iter = map.keys();
108         while (iter.hasNext()) {
109             String syscallName = iter.next();
110             testBlocked(map.getInt(syscallName));
111         }
112     }
113 
114     class IncomingHandler extends Handler {
IncomingHandler(Looper looper)115         IncomingHandler(Looper looper) {
116             super(looper);
117         }
118 
119         @Override
handleMessage(Message msg)120         public void handleMessage(Message msg) {
121             switch (msg.what) {
122                 case IsolatedService.MSG_SECCOMP_RESULT:
123                     mAppZygoteResult = (msg.arg1 == 1);
124                     mResultCondition.open();
125                     break;
126                 default:
127                     super.handleMessage(msg);
128             }
129         }
130 	}
131 
132     class IsolatedConnection implements ServiceConnection {
133         private final ConditionVariable mConnectedCondition = new ConditionVariable();
134         private Messenger mService;
135 
onServiceConnected(ComponentName name, IBinder binder)136         public void onServiceConnected(ComponentName name, IBinder binder) {
137             mService = new Messenger(binder);
138             mConnectedCondition.open();
139         }
140 
onServiceDisconnected(ComponentName name)141         public void onServiceDisconnected(ComponentName name) {
142             mConnectedCondition.close();
143         }
144 
getTestResult()145         public boolean getTestResult() {
146             boolean connected = mConnectedCondition.block(SERVICE_START_TIMEOUT_MS);
147             if (!connected) {
148                Log.e(TAG, "Failed to wait for IsolatedService to bind.");
149                return false;
150             }
151             Message msg = Message.obtain(null, IsolatedService.MSG_GET_SECCOMP_RESULT);
152             msg.replyTo = mMessenger;
153             try {
154                 mService.send(msg);
155             } catch (RemoteException e) {
156                 Log.e(TAG, "Failed to send message to IsolatedService to get test result.", e);
157                 return false;
158             }
159 
160             // Wait for result and return it
161             mResultCondition.block();
162 
163             return mAppZygoteResult;
164         }
165     }
166 
bindService(Class<?> cls)167     private IsolatedConnection bindService(Class<?> cls) {
168         Intent intent = new Intent();
169         intent.setClass(mContext, cls);
170         IsolatedConnection conn = new IsolatedConnection();
171         mContext.bindService(intent, conn, Context.BIND_AUTO_CREATE);
172 
173         return conn;
174     }
175 
176     @Test
testAppZygoteSyscalls()177     public void testAppZygoteSyscalls() {
178         // Isolated services that spawn from the application Zygote are allowed
179         // to preload code in a security context that is allowed to use
180         // setresuid() / setresgid() in a limited range; this test enforces
181         // we allow calls within the range, and reject those outside them.
182         // This is done from the ZygotePreload class (which runs in the app zygote
183         // context); here we just wait for the service to come up, and ask it
184         // whether the tests were executed successfully. We have to ask the service
185         // because that is the only process that we can talk to that shares the
186         // same address space as the ZygotePreload class, which holds the test
187         // result.
188         IsolatedConnection conn = bindService(IsolatedService.class);
189         boolean testResult = conn.getTestResult();
190         Assert.assertTrue("seccomp tests in application zygote failed; see logs.", testResult);
191     }
192 
getCurrentArch()193     private static String getCurrentArch() {
194         if (CpuFeatures.isArm64Cpu()) {
195             return "arm64";
196         } else if (CpuFeatures.isArmCpu()) {
197             return "arm";
198         } else if (CpuFeatures.isX86_64Cpu()) {
199             return "x86_64";
200         } else if (CpuFeatures.isX86Cpu()) {
201             return "x86";
202         } else {
203             Assert.fail("Unsupported architecture");
204             return null;
205         }
206     }
207 
readInputStreamFully(InputStream is)208     private String readInputStreamFully(InputStream is) throws IOException {
209         StringBuilder sb = new StringBuilder();
210         byte[] buffer = new byte[4096];
211         while (is.available() > 0) {
212             int size = is.read(buffer);
213             if (size < 0) {
214                 break;
215             }
216             sb.append(new String(buffer, 0, size));
217         }
218         return sb.toString();
219     }
220 
testBlocked(int nr)221     private void testBlocked(int nr) {
222         Assert.assertTrue("Syscall " + nr + " not blocked", testSyscallBlocked(nr));
223     }
224 
testAllowed(int nr)225     private void testAllowed(int nr) {
226         Assert.assertFalse("Syscall " + nr + " blocked", testSyscallBlocked(nr));
227     }
228 
testSyscallBlocked(int nr)229     private static final native boolean testSyscallBlocked(int nr);
testSetresuidBlocked(int ruid, int euid, int suid)230     protected static final native boolean testSetresuidBlocked(int ruid, int euid, int suid);
testSetresgidBlocked(int rgid, int egid, int sgid)231     protected static final native boolean testSetresgidBlocked(int rgid, int egid, int sgid);
232 }
233