autotest (wifi): test malformed probe responses

Add autotest for malformed probe response packets.  This test covers
probe responses that include an element with an incorrect tag length.

This test covers a case where a malformed probe response may have been
the cause of a lucas device wifi disconnect.  The probe response had
an incorrect length field.  This test reproduces the scenario by
triggering a valid probe request.  The AP in the test is configured to
repeatedly send out unsolicited (malformed) probe responses.  To reduce
test flake, multiple attempts are made to receive a probe response.
The test fails if no probe responses are received, the device
disconnects or if the wifi card resets.

BUG=chromium:498018
TEST=tested on pit and lucas in wifi dev cell

Change-Id: Ib1902ab21503c65ce5028b3395ddf6f37de6888c
Reviewed-on: https://chromium-review.googlesource.com/278251
Reviewed-by: mukesh agrawal <quiche@chromium.org>
Tested-by: Rebecca Silberstein <silberst@chromium.org>
Commit-Queue: Rebecca Silberstein <silberst@chromium.org>
diff --git a/server/cros/network/frame_sender.py b/server/cros/network/frame_sender.py
index f73e703..d266847 100644
--- a/server/cros/network/frame_sender.py
+++ b/server/cros/network/frame_sender.py
@@ -6,7 +6,8 @@
     """Context manager for sending management frames."""
 
     def __init__(self, router, frame_type, channel, ssid_prefix=None,
-                 num_bss=None, frame_count=None, delay=None):
+                 num_bss=None, frame_count=None, delay=None, dest_addr=None,
+                 probe_resp_footer=None):
         """
         @param router: LinuxRouter object router to send frames from.
         @param frame_type: int management frame type.
@@ -16,6 +17,8 @@
         @param frame_count: int number of frames to send, frame_count of 0
                 implies infinite number of frames.
         @param delay: int delay in between frames in milliseconds.
+        @param dest_addr: MAC address of the destination address (DA).
+        @param probe_resp_footer: footer bytes for probe responses.
         """
         self._router = router
         self._channel = channel
@@ -24,6 +27,8 @@
         self._num_bss = num_bss
         self._frame_count = frame_count
         self._delay = delay
+        self._dest_addr = dest_addr
+        self._probe_resp_footer = probe_resp_footer
         self._interface = None
         self._pid = None
 
@@ -33,7 +38,8 @@
         self._pid = self._router.send_management_frame(self._interface,
                 self._frame_type, self._channel, ssid_prefix=self._ssid_prefix,
                 num_bss=self._num_bss, frame_count=self._frame_count,
-                delay=self._delay)
+                delay=self._delay, dest_addr=self._dest_addr,
+                probe_resp_footer=self._probe_resp_footer)
         return self
 
 
diff --git a/server/cros/network/wifi_client.py b/server/cros/network/wifi_client.py
index d18a19b..6ea4a51 100644
--- a/server/cros/network/wifi_client.py
+++ b/server/cros/network/wifi_client.py
@@ -992,6 +992,17 @@
         return self.assert_disconnect_count(0)
 
 
+    def get_num_card_resets(self):
+        """Get card reset count."""
+        reset_msg = 'mwifiex_sdio_card_reset'
+        result = self.host.run('grep -c %s /var/log/messages' % reset_msg,
+                               ignore_status=True)
+        if result.exit_status == 1:
+            return 0
+        count = int(result.stdout.strip())
+        return count
+
+
     def release_wifi_if(self):
         """Release the control over the wifi interface back to normal operation.
 
diff --git a/server/site_linux_router.py b/server/site_linux_router.py
index 3626c08..43999b3 100644
--- a/server/site_linux_router.py
+++ b/server/site_linux_router.py
@@ -7,6 +7,7 @@
 import logging
 import random
 import string
+import tempfile
 import time
 
 from autotest_lib.client.common_lib import error
@@ -105,6 +106,8 @@
 
     MGMT_FRAME_SENDER_LOG_FILE = '/tmp/send_management_frame-test.log'
 
+    PROBE_RESPONSE_FOOTER_FILE = '/tmp/autotest-probe_response_footer'
+
     def get_capabilities(self):
         """@return iterable object of AP capabilities for this system."""
         caps = set([self.CAPABILITY_IBSS])
@@ -818,6 +821,22 @@
                         (self.cmd_hostapd_cli, control_if, client_mac))
 
 
+    def _prep_probe_response_footer(self, footer):
+        """Write probe response footer temporarily to a local file and copy
+        over to test router.
+
+        @param footer string containing bytes for the probe response footer.
+        @raises AutoservRunError: If footer file copy fails.
+
+        """
+        with tempfile.TemporaryFile() as fp:
+            fp.write(footer)
+            try:
+                self.host.send_file(fp, self.PROBE_RESPONSE_FOOTER_FILE)
+            except error.AutoservRunError:
+                logging.error('failed to copy footer file to AP')
+                raise
+
     def send_management_frame_on_ap(self, frame_type, channel, instance=0):
         """Injects a management frame into an active hostapd session.
 
@@ -854,7 +873,8 @@
 
     def send_management_frame(self, interface, frame_type, channel,
                               ssid_prefix=None, num_bss=None,
-                              frame_count=None, delay=None):
+                              frame_count=None, delay=None,
+                              dest_addr=None, probe_resp_footer=None):
         """
         Injects management frames on specify channel |frequency|.
 
@@ -868,6 +888,8 @@
         @param num_bss int number of BSS.
         @param frame_count int number of frames to send.
         @param delay int milliseconds delay between frames.
+        @param dest_addr string destination address (DA) MAC address.
+        @param probe_resp_footer string footer for probe response.
 
         @return int PID of the newly created process.
 
@@ -882,6 +904,11 @@
             command += ' -n %d' % (frame_count)
         if delay is not None:
             command += ' -d %d' % (delay)
+        if dest_addr is not None:
+            command += ' -a %s' % (dest_addr)
+        if probe_resp_footer is not None:
+            self._prep_probe_response_footer(footer=probe_resp_footer)
+            command += ' -f %s' % (self.PROBE_RESPONSE_FOOTER_FILE)
         command += ' > %s 2>&1 & echo $!' % (self.MGMT_FRAME_SENDER_LOG_FILE)
         pid = int(self.router.run(command).stdout)
         return pid
diff --git a/server/site_tests/network_WiFi_MalformedProbeResp/control b/server/site_tests/network_WiFi_MalformedProbeResp/control
new file mode 100644
index 0000000..fe092c1
--- /dev/null
+++ b/server/site_tests/network_WiFi_MalformedProbeResp/control
@@ -0,0 +1,29 @@
+# Copyright 2015 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+AUTHOR = 'silberst, pstew, quiche'
+NAME = 'network_WiFi_MalformedProbeResp'
+TIME = 'MEDIUM'
+TEST_TYPE = 'Server'
+# TODO(silberst): add to wifi test suites crbug.com/505662
+# ATTRIBUTES = "suite:wifi_correctness_cros_core, suite:wifi_matfunc, suite:wifi_matfunc_bcm4356, suite:wifi_matfunc_marvell8897, suite:wifi_release"
+# SUITE = ('wifi_matfunc, wifi_matfunc_bcm4356, wifi_matfunc_marvell8897,'
+#         'wifi_correctness_cros_core, wifi_release')
+DEPENDENCIES = 'wificell'
+
+DOC = """
+This test attempts to verify that we can stay connected to a router even
+if we receive malformed probe responses.  In this particular case, the
+probe response data has a tag with an incorrect length.
+"""
+
+
+def run(machine):
+    host = hosts.create_host(machine)
+    job.run_test('network_WiFi_MalformedProbeResp',
+                 host=host,
+                 raw_cmdline_args=args)
+
+
+parallel_simple(run, machines)
diff --git a/server/site_tests/network_WiFi_MalformedProbeResp/network_WiFi_MalformedProbeResp.py b/server/site_tests/network_WiFi_MalformedProbeResp/network_WiFi_MalformedProbeResp.py
new file mode 100644
index 0000000..35ae51c
--- /dev/null
+++ b/server/site_tests/network_WiFi_MalformedProbeResp/network_WiFi_MalformedProbeResp.py
@@ -0,0 +1,86 @@
+# Copyright (c) 2015 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import logging
+import time
+
+from autotest_lib.client.common_lib import error
+from autotest_lib.client.common_lib.cros.network import xmlrpc_datatypes
+from autotest_lib.server import site_linux_system
+from autotest_lib.server.cros.network import frame_sender
+from autotest_lib.server.cros.network import hostap_config
+from autotest_lib.server.cros.network import wifi_cell_test_base
+
+class network_WiFi_MalformedProbeResp(wifi_cell_test_base.WiFiCellTestBase):
+    """Test that we can stay connected to the configured AP when receiving
+    malformed probe responses from an AP that we are not connected to."""
+    version = 1
+
+    PROBE_RESPONSE_DELAY_MSEC = 50
+    SCAN_LOOP_SEC = 60
+    SCAN_LOOP_SLEEP_SEC = 10
+    PROBE_RESPONSE_TEST_CHANNEL = 1
+
+    def run_once(self):
+        """Sets up a router, connects to it, pings it, and repeats."""
+        configuration = hostap_config.HostapConfig(
+                channel=self.PROBE_RESPONSE_TEST_CHANNEL,
+                mode=hostap_config.HostapConfig.MODE_11B)
+        self.context.router.require_capabilities(
+            [site_linux_system.LinuxSystem.CAPABILITY_SEND_MANAGEMENT_FRAME])
+
+        self.context.configure(configuration)
+        client_mac = self.context.client.wifi_mac
+
+        pretest_reset_count = self.context.client.get_num_card_resets()
+        logging.debug('pretest_reset_count=%d', pretest_reset_count)
+        self.context.router.start_capture(
+            configuration.frequency,
+            ht_type=configuration.ht_packet_capture_mode)
+        assoc_params = xmlrpc_datatypes.AssociationParameters()
+        assoc_params.ssid = self.context.router.get_ssid()
+        assoc_result = self.context.assert_connect_wifi(assoc_params)
+        start_time = time.time()
+        count = 1
+        scan = 0
+        rx_probe_resp_count = 0
+        with self.context.client.assert_no_disconnects():
+            with frame_sender.FrameSender(
+                    self.context.router,
+                    'probe_response',
+                    self.PROBE_RESPONSE_TEST_CHANNEL,
+                    ssid_prefix='TestingProbes',
+                    num_bss=1,
+                    frame_count=0,
+                    delay=self.PROBE_RESPONSE_DELAY_MSEC,
+                    dest_addr=client_mac,
+                    probe_resp_footer='\xdd\xb7\x00\x1a\x11\x01\x01\x02\x03'):
+                while time.time() - start_time < self.SCAN_LOOP_SEC:
+                    bss_list = self.context.client.iw_runner.scan(
+                            self.context.client.wifi_if, [2412])
+                    for bss in bss_list:
+                        logging.debug('found bss: %s', bss.ssid)
+                        if bss.ssid == 'TestingProbes00000000':
+                            rx_probe_resp_count += 1
+                    time.sleep(self.SCAN_LOOP_SLEEP_SEC)
+                else:
+                    logging.debug('done scanning for networks')
+
+        logging.debug('received %s probe_responses', rx_probe_resp_count)
+        if rx_probe_resp_count == 0:
+            raise error.TestFail('Client failed to receive probe responses')
+
+        reset_count = self.context.client.get_num_card_resets()
+        logging.debug('reset count = %s', reset_count)
+        test_resets = reset_count - pretest_reset_count
+        if test_resets < 0:
+            logging.debug('logs rotated during test')
+            if reset_count > 0:
+                test_resets = reset_count
+
+        if test_resets > 0:
+            raise error.TestFail('Client reset card')
+        self.context.client.shill.disconnect(assoc_params.ssid)
+        self.context.router.deconfig()
+        self.context.router.stop_capture()