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