1 /*
2  * Copyright (C) 2016 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 package android.security.cts;
17 
18 import android.media.AudioFormat;
19 import android.media.AudioManager;
20 import android.media.AudioTrack;
21 import android.media.audiofx.AudioEffect;
22 import android.media.audiofx.Equalizer;
23 import android.platform.test.annotations.SecurityTest;
24 import android.util.Log;
25 
26 import com.android.compatibility.common.util.CtsAndroidTestCase;
27 
28 import java.nio.ByteBuffer;
29 import java.nio.ByteOrder;
30 import java.util.Arrays;
31 import java.util.UUID;
32 
33 @SecurityTest
34 public class AudioSecurityTest extends CtsAndroidTestCase {
35     private static final String TAG = "AudioSecurityTest";
36 
37     private static final int ERROR_DEAD_OBJECT = -7; // AudioEffect.ERROR_DEAD_OBJECT
38 
39     // should match audio_effect.h (native)
40     private static final int EFFECT_CMD_SET_PARAM = 5;
41     private static final int EFFECT_CMD_GET_PARAM = 8;
42     private static final int EFFECT_CMD_OFFLOAD   = 20;
43     private static final int SIZEOF_EFFECT_PARAM_T = 12;
44 
verifyZeroReply(byte[] reply)45     private static void verifyZeroReply(byte[] reply) throws Exception {
46         int count = 0;
47         for (byte b : reply) {
48             if (b != 0) {
49                 count++;
50             }
51         }
52         assertEquals("reply has " + count + " nonzero values", 0 /* expected */, count);
53     }
54 
55     // @FunctionalInterface
56     private interface TestEffect {
test(AudioEffect audioEffect)57         void test(AudioEffect audioEffect) throws Exception;
58     }
59 
testAllEffects(String testName, TestEffect testEffect)60     private static void testAllEffects(String testName, TestEffect testEffect) throws Exception {
61         int failures = 0;
62         for (AudioEffect.Descriptor descriptor : AudioEffect.queryEffects()) {
63             final AudioEffect audioEffect;
64             try {
65                 audioEffect = (AudioEffect)AudioEffect.class.getConstructor(
66                         UUID.class, UUID.class, int.class, int.class).newInstance(
67                                 descriptor.type,
68                                 descriptor.uuid, // uuid overrides type
69                                 0 /* priority */, 0 /* audioSession */);
70             } catch (Exception e) {
71                 Log.w(TAG, "effect " + testName + " " + descriptor.name
72                         + " cannot be created (ignoring)");
73                 continue; // OK;
74             }
75             try {
76                 testEffect.test(audioEffect);
77                 Log.d(TAG, "effect " + testName + " " + descriptor.name + " success");
78             } catch (Exception e) {
79                 Log.e(TAG, "effect " + testName + " " + descriptor.name + " exception failed!",
80                         e);
81                 ++failures;
82             } catch (AssertionError e) {
83                 Log.e(TAG, "effect " + testName + " " + descriptor.name + " assert failed!",
84                         e);
85                 ++failures;
86             }
87         }
88         assertEquals("found " + testName + " " + failures + " failures",
89                 0 /* expected */, failures);
90     }
91 
92     // b/28173666
93     @SecurityTest(minPatchLevel = "2016-07")
testAllEffectsGetParameterAttemptOffload_CVE_2016_3745()94     public void testAllEffectsGetParameterAttemptOffload_CVE_2016_3745() throws Exception {
95         testAllEffects("get parameter attempt offload",
96                 new TestEffect() {
97             @Override
98             public void test(AudioEffect audioEffect) throws Exception {
99                 testAudioEffectGetParameter(audioEffect, true /* offload */);
100             }
101         });
102     }
103 
104     // b/32438594
105     // b/32624850
106     // b/32635664
107     @SecurityTest(minPatchLevel = "2017-03")
testAllEffectsGetParameter2AttemptOffload_CVE_2017_0398()108     public void testAllEffectsGetParameter2AttemptOffload_CVE_2017_0398() throws Exception {
109         testAllEffects("get parameter2 attempt offload",
110                 new TestEffect() {
111             @Override
112             public void test(AudioEffect audioEffect) throws Exception {
113                 testAudioEffectGetParameter2(audioEffect, true /* offload */);
114             }
115         });
116     }
117 
118     // b/30204301
119     @SecurityTest(minPatchLevel = "2016-10")
testAllEffectsSetParameterAttemptOffload_CVE_2016_3924()120     public void testAllEffectsSetParameterAttemptOffload_CVE_2016_3924() throws Exception {
121         testAllEffects("set parameter attempt offload",
122                 new TestEffect() {
123             @Override
124             public void test(AudioEffect audioEffect) throws Exception {
125                 testAudioEffectSetParameter(audioEffect, true /* offload */);
126             }
127         });
128     }
129 
130     // b/37536407
131     @SecurityTest(minPatchLevel = "2017-01")
testAllEffectsEqualizer_CVE_2017_0401()132     public void testAllEffectsEqualizer_CVE_2017_0401() throws Exception {
133         testAllEffects("equalizer get parameter name",
134                 new TestEffect() {
135             @Override
136             public void test(AudioEffect audioEffect) throws Exception {
137                 testAudioEffectEqualizerGetParameterName(audioEffect);
138             }
139         });
140     }
141 
testAudioEffectGetParameter( AudioEffect audioEffect, boolean offload)142     private static void testAudioEffectGetParameter(
143             AudioEffect audioEffect, boolean offload) throws Exception {
144         if (audioEffect == null) {
145             return;
146         }
147         try {
148             // 1) set offload_enabled
149             if (offload) {
150                 byte command[] = new byte[8];
151                 Arrays.fill(command, (byte)1);
152                 byte reply[] = new byte[4]; // ignored
153 
154                 /* ignored */ AudioEffect.class.getDeclaredMethod(
155                         "command", int.class, byte[].class, byte[].class).invoke(
156                                 audioEffect, EFFECT_CMD_OFFLOAD, command, reply);
157             }
158 
159             // 2) get parameter with invalid psize
160             {
161                 byte command[] = new byte[30];
162                 Arrays.fill(command, (byte)0xDD);
163                 byte reply[] = new byte[30];
164 
165                 Integer ret = (Integer) AudioEffect.class.getDeclaredMethod(
166                         "command", int.class, byte[].class, byte[].class).invoke(
167                                 audioEffect, EFFECT_CMD_GET_PARAM, command, reply);
168 
169                 assertTrue("Audio server might have crashed", ret != ERROR_DEAD_OBJECT);
170                 verifyZeroReply(reply);
171             }
172 
173             // NOTE: an alternative way of checking crash:
174             //
175             // Thread.sleep(1000 /* millis */);
176             // assertTrue("Audio server might have crashed",
177             //        audioEffect.setEnabled(false) != AudioEffect.ERROR_DEAD_OBJECT);
178         } catch (NoSuchMethodException e) {
179             Log.w(TAG, "AudioEffect.command() does not exist (ignoring)"); // OK
180         } finally {
181             audioEffect.release();
182         }
183     }
184 
testAudioEffectGetParameter2( AudioEffect audioEffect, boolean offload)185     private static void testAudioEffectGetParameter2(
186             AudioEffect audioEffect, boolean offload) throws Exception {
187         if (audioEffect == null) {
188             return;
189         }
190         try {
191             // 1) set offload_enabled
192             if (offload) {
193                 byte command[] = new byte[8];
194                 Arrays.fill(command, (byte)1);
195                 byte reply[] = new byte[4]; // ignored
196 
197                 /* ignored */ AudioEffect.class.getDeclaredMethod(
198                         "command", int.class, byte[].class, byte[].class).invoke(
199                                 audioEffect, EFFECT_CMD_OFFLOAD, command, reply);
200             }
201 
202             // 2) get parameter with small command size but large psize
203             {
204                 final int parameterSize = 0x100000;
205 
206                 byte command[] = ByteBuffer.allocate(5 * 4 /* capacity */)
207                         .order(ByteOrder.nativeOrder())
208                         .putInt(0)             // status (unused)
209                         .putInt(parameterSize) // psize (very large)
210                         .putInt(0)             // vsize
211                         .putInt(0x04030201)    // data[0] (param too small for psize)
212                         .putInt(0x08070605)    // data[4]
213                         .array();
214                 byte reply[] = new byte[parameterSize + SIZEOF_EFFECT_PARAM_T];
215 
216                 Integer ret = (Integer) AudioEffect.class.getDeclaredMethod(
217                         "command", int.class, byte[].class, byte[].class).invoke(
218                                 audioEffect, EFFECT_CMD_GET_PARAM, command, reply);
219 
220                 verifyZeroReply(reply);
221                 assertTrue("Audio server might have crashed", ret != ERROR_DEAD_OBJECT);
222             }
223         } catch (NoSuchMethodException e) {
224             Log.w(TAG, "AudioEffect.command() does not exist (ignoring)"); // OK
225         } finally {
226             audioEffect.release();
227         }
228     }
229 
testAudioEffectGetParameter3(AudioEffect audioEffect)230     private static void testAudioEffectGetParameter3(AudioEffect audioEffect) throws Exception {
231         if (audioEffect == null) {
232             return;
233         }
234         try {
235             // 1) get parameter with zero command size
236             {
237                 final int parameterSize = 0x10;
238 
239                 Integer ret = (Integer) AudioEffect.class.getDeclaredMethod(
240                         "command", int.class, byte[].class, byte[].class).invoke(
241                                 audioEffect,
242                                 EFFECT_CMD_GET_PARAM,
243                                 new byte[0] /* command */,
244                                 new byte[parameterSize + SIZEOF_EFFECT_PARAM_T] /* reply */);
245 
246                 assertTrue("Audio server might have crashed", ret != ERROR_DEAD_OBJECT);
247             }
248         } catch (NoSuchMethodException e) {
249             Log.w(TAG, "AudioEffect.command() does not exist (ignoring)"); // OK
250         } finally {
251             audioEffect.release();
252         }
253     }
254 
testAudioEffectSetParameter( AudioEffect audioEffect, boolean offload)255     private static void testAudioEffectSetParameter(
256             AudioEffect audioEffect, boolean offload) throws Exception {
257         if (audioEffect == null) {
258             return;
259         }
260         try {
261             // 1) set offload_enabled
262             if (offload) {
263                 byte command[] = new byte[8];
264                 Arrays.fill(command, (byte)1);
265                 byte reply[] = new byte[4]; // ignored
266 
267                 /* ignored */ AudioEffect.class.getDeclaredMethod(
268                         "command", int.class, byte[].class, byte[].class).invoke(
269                                 audioEffect, EFFECT_CMD_OFFLOAD, command, reply);
270             }
271 
272             // 2) set parameter with invalid psize
273             {
274                 byte command[] = ByteBuffer.allocate(5 * 4 /* capacity */)
275                         .order(ByteOrder.nativeOrder())
276                         .putInt(0)          // status (unused)
277                         .putInt(0xdddddddd) // psize (very large)
278                         .putInt(4)          // vsize
279                         .putInt(1)          // data[0] (param too small for psize)
280                         .putInt(0)          // data[4]
281                         .array();
282                 byte reply[] = new byte[4]; // returns status code (ignored)
283 
284                 Integer ret = (Integer) AudioEffect.class.getDeclaredMethod(
285                         "command", int.class, byte[].class, byte[].class).invoke(
286                                 audioEffect, EFFECT_CMD_SET_PARAM, command, reply);
287 
288                 assertTrue("Audio server might have crashed", ret != ERROR_DEAD_OBJECT);
289                 // on failure reply may contain the status code.
290             }
291         } catch (NoSuchMethodException e) {
292             Log.w(TAG, "AudioEffect.command() does not exist (ignoring)"); // OK
293         } finally {
294             audioEffect.release();
295         }
296     }
297 
testAudioEffectSetOffload(AudioEffect audioEffect)298     private static void testAudioEffectSetOffload(AudioEffect audioEffect) throws Exception {
299         if (audioEffect == null) {
300             return;
301         }
302         try {
303             // 1) set offload_enabled with zero command and reply size
304             {
305                 Integer ret = (Integer) AudioEffect.class.getDeclaredMethod(
306                         "command", int.class, byte[].class, byte[].class).invoke(
307                                 audioEffect,
308                                 EFFECT_CMD_OFFLOAD,
309                                 new byte[0] /* command */,
310                                 new byte[0] /* reply */);
311 
312                 assertTrue("Audio server might have crashed", ret != ERROR_DEAD_OBJECT);
313             }
314          } catch (NoSuchMethodException e) {
315             Log.w(TAG, "AudioEffect.command() does not exist (ignoring)"); // OK
316         } finally {
317             audioEffect.release();
318         }
319     }
320 
testAudioEffectEqualizerGetParameterName( AudioEffect audioEffect)321     private static void testAudioEffectEqualizerGetParameterName(
322             AudioEffect audioEffect) throws Exception {
323         if (audioEffect == null) {
324             return;
325         }
326         try {
327             // get parameter name with zero vsize
328             {
329                 final int param = Equalizer.PARAM_GET_PRESET_NAME;
330                 final int band = 0;
331                 byte command[] = ByteBuffer.allocate(5 * 4 /* capacity */)
332                         .order(ByteOrder.nativeOrder())
333                         .putInt(0)          // status (unused)
334                         .putInt(8)          // psize (param, band)
335                         .putInt(0)          // vsize
336                         .putInt(param)      // equalizer param
337                         .putInt(band)       // equalizer band
338                         .array();
339                 Integer ret = (Integer) AudioEffect.class.getDeclaredMethod(
340                         "command", int.class, byte[].class, byte[].class).invoke(
341                                 audioEffect, EFFECT_CMD_GET_PARAM, command,
342                                 new byte[5 * 4] /* reply - ignored */);
343                 assertTrue("Audio server might have crashed", ret != ERROR_DEAD_OBJECT);
344             }
345         } catch (NoSuchMethodException e) {
346             Log.w(TAG, "AudioEffect.command() does not exist (ignoring)"); // OK
347         } finally {
348             audioEffect.release();
349         }
350     }
351 
352     // should match effect_visualizer.h (native)
353     private static final String VISUALIZER_TYPE = "e46b26a0-dddd-11db-8afd-0002a5d5c51b";
354     private static final int VISUALIZER_CMD_CAPTURE = 0x10000;
355     private static final int VISUALIZER_PARAM_CAPTURE_SIZE = 0;
356 
357     // b/31781965
358     @SecurityTest(minPatchLevel = "2017-03")
testVisualizerCapture_CVE_2017_0396()359     public void testVisualizerCapture_CVE_2017_0396() throws Exception {
360         // Capture params
361         final int CAPTURE_SIZE = 1 << 24; // 16MB seems to be large enough to cause a SEGV.
362         final byte[] captureBuf = new byte[CAPTURE_SIZE];
363 
364         // Track params
365         final int sampleRate = 48000;
366         final int format = AudioFormat.ENCODING_PCM_16BIT;
367         final int loops = 1;
368         final int seconds = 1;
369         final int channelCount = 2;
370         final int bufferFrames = seconds * sampleRate;
371         final int bufferSamples = bufferFrames * channelCount;
372         final int bufferSize = bufferSamples * 2; // bytes per sample for 16 bits
373         final short data[] = new short[bufferSamples]; // zero data
374 
375         for (AudioEffect.Descriptor descriptor : AudioEffect.queryEffects()) {
376             if (descriptor.type.compareTo(UUID.fromString(VISUALIZER_TYPE)) != 0) {
377                 continue;
378             }
379 
380             AudioEffect audioEffect = null;
381             AudioTrack audioTrack = null;
382 
383             try {
384                 // create track and play
385                 {
386                     audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate,
387                             AudioFormat.CHANNEL_OUT_STEREO, format, bufferSize,
388                             AudioTrack.MODE_STATIC);
389                     assertEquals("Cannot write to audio track",
390                             bufferSamples,
391                             audioTrack.write(data, 0 /* offsetInBytes */, data.length));
392                     assertEquals("AudioTrack not initialized",
393                             AudioTrack.STATE_INITIALIZED,
394                             audioTrack.getState());
395                     assertEquals("Cannot set loop points",
396                             android.media.AudioTrack.SUCCESS,
397                             audioTrack.setLoopPoints(0 /* startInFrames */, bufferFrames, loops));
398                     audioTrack.play();
399                 }
400 
401                 // wait for track to really begin playing
402                 Thread.sleep(200 /* millis */);
403 
404                 // create effect
405                 {
406                     audioEffect = (AudioEffect) AudioEffect.class.getConstructor(
407                             UUID.class, UUID.class, int.class, int.class).newInstance(
408                                     descriptor.type, descriptor.uuid, 0 /* priority */,
409                                     audioTrack.getAudioSessionId());
410                 }
411 
412                 // set capture size
413                 {
414                     byte command[] = ByteBuffer.allocate(5 * 4 /* capacity */)
415                             .order(ByteOrder.nativeOrder())
416                             .putInt(0)                             // status (unused)
417                             .putInt(4)                             // psize (sizeof(param))
418                             .putInt(4)                             // vsize (sizeof(value))
419                             .putInt(VISUALIZER_PARAM_CAPTURE_SIZE) // data[0] (param)
420                             .putInt(CAPTURE_SIZE)                  // data[4] (value)
421                             .array();
422 
423                     Integer ret = (Integer) AudioEffect.class.getDeclaredMethod(
424                             "command", int.class, byte[].class, byte[].class).invoke(
425                                     audioEffect,
426                                     EFFECT_CMD_SET_PARAM,
427                                     command, new byte[4] /* reply */);
428                     Log.d(TAG, "setparam returns " + ret);
429                     assertTrue("Audio server might have crashed", ret != ERROR_DEAD_OBJECT);
430                 }
431 
432                 // enable effect
433                 {
434                     final int ret = audioEffect.setEnabled(true);
435                     assertEquals("Cannot enable audio effect", 0 /* expected */, ret);
436                 }
437 
438                 // wait for track audio data to be processed, otherwise capture
439                 // will not really return audio data.
440                 Thread.sleep(200 /* millis */);
441 
442                 // capture data
443                 {
444                     Integer ret = (Integer) AudioEffect.class.getDeclaredMethod(
445                             "command", int.class, byte[].class, byte[].class).invoke(
446                                     audioEffect,
447                                     VISUALIZER_CMD_CAPTURE,
448                                     new byte[0] /* command */, captureBuf /* reply */);
449                     Log.d(TAG, "capture returns " + ret);
450                     assertTrue("Audio server might have crashed", ret != ERROR_DEAD_OBJECT);
451                 }
452             } finally {
453                 if (audioEffect != null) {
454                     audioEffect.release();
455                 }
456                 if (audioTrack != null) {
457                     audioTrack.release();
458                 }
459             }
460         }
461     }
462 }
463