Home | History | Annotate | Download | only in cts
      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 
     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 {
     57         void test(AudioEffect audioEffect) throws Exception;
     58     }
     59 
     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")
     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")
    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")
    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")
    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 
    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 
    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 
    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 
    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 
    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 
    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")
    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