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                 default:
126                     super.handleMessage(msg);
127             }
128         }
129 	}
130 
131     class IsolatedConnection implements ServiceConnection {
132         private final ConditionVariable mConnectedCondition = new ConditionVariable();
133         private Messenger mService;
134 
onServiceConnected(ComponentName name, IBinder binder)135         public void onServiceConnected(ComponentName name, IBinder binder) {
136             mService = new Messenger(binder);
137             mConnectedCondition.open();
138         }
139 
onServiceDisconnected(ComponentName name)140         public void onServiceDisconnected(ComponentName name) {
141             mConnectedCondition.close();
142         }
143 
getTestResult()144         public boolean getTestResult() {
145             boolean connected = mConnectedCondition.block(SERVICE_START_TIMEOUT_MS);
146             if (!connected) {
147                Log.e(TAG, "Failed to wait for IsolatedService to bind.");
148                return false;
149             }
150             Message msg = Message.obtain(null, IsolatedService.MSG_GET_SECCOMP_RESULT);
151             msg.replyTo = mMessenger;
152             try {
153                 mService.send(msg);
154             } catch (RemoteException e) {
155                 Log.e(TAG, "Failed to send message to IsolatedService to get test result.", e);
156                 return false;
157             }
158 
159             // Wait for result and return it
160             mResultCondition.block();
161 
162             return mAppZygoteResult;
163         }
164     }
165 
bindService(Class<?> cls)166     private IsolatedConnection bindService(Class<?> cls) {
167         Intent intent = new Intent();
168         intent.setClass(mContext, cls);
169         IsolatedConnection conn = new IsolatedConnection();
170         mContext.bindService(intent, conn, Context.BIND_AUTO_CREATE);
171 
172         return conn;
173     }
174 
175     @Test
testAppZygoteSyscalls()176     public void testAppZygoteSyscalls() {
177         // Isolated services that spawn from the application Zygote are allowed
178         // to preload code in a security context that is allowed to use
179         // setresuid() / setresgid() in a limited range; this test enforces
180         // we allow calls within the range, and reject those outside them.
181         // This is done from the ZygotePreload class (which runs in the app zygote
182         // context); here we just wait for the service to come up, and ask it
183         // whether the tests were executed successfully. We have to ask the service
184         // because that is the only process that we can talk to that shares the
185         // same address space as the ZygotePreload class, which holds the test
186         // result.
187         IsolatedConnection conn = bindService(IsolatedService.class);
188         boolean testResult = conn.getTestResult();
189         Assert.assertTrue("seccomp tests in application zygote failed; see logs.", testResult);
190     }
191 
getCurrentArch()192     private static String getCurrentArch() {
193         if (CpuFeatures.isArm64Cpu()) {
194             return "arm64";
195         } else if (CpuFeatures.isArmCpu()) {
196             return "arm";
197         } else if (CpuFeatures.isX86_64Cpu()) {
198             return "x86_64";
199         } else if (CpuFeatures.isX86Cpu()) {
200             return "x86";
201         } else {
202             Assert.fail("Unsupported architecture");
203             return null;
204         }
205     }
206 
readInputStreamFully(InputStream is)207     private String readInputStreamFully(InputStream is) throws IOException {
208         StringBuilder sb = new StringBuilder();
209         byte[] buffer = new byte[4096];
210         while (is.available() > 0) {
211             int size = is.read(buffer);
212             if (size < 0) {
213                 break;
214             }
215             sb.append(new String(buffer, 0, size));
216         }
217         return sb.toString();
218     }
219 
testBlocked(int nr)220     private void testBlocked(int nr) {
221         Assert.assertTrue("Syscall " + nr + " not blocked", testSyscallBlocked(nr));
222     }
223 
testAllowed(int nr)224     private void testAllowed(int nr) {
225         Assert.assertFalse("Syscall " + nr + " blocked", testSyscallBlocked(nr));
226     }
227 
testSyscallBlocked(int nr)228     private static final native boolean testSyscallBlocked(int nr);
testSetresuidBlocked(int ruid, int euid, int suid)229     protected static final native boolean testSetresuidBlocked(int ruid, int euid, int suid);
testSetresgidBlocked(int rgid, int egid, int sgid)230     protected static final native boolean testSetresgidBlocked(int rgid, int egid, int sgid);
231 }
232