diff --git a/framework/crb.py b/framework/crb.py
index a47de2c..d456fab 100644
--- a/framework/crb.py
+++ b/framework/crb.py
@@ -499,27 +499,31 @@ class Crb(object):
         pids = []
         pid_reg = r'p(\d+)'
         for config_file in file_directorys:
-            cmd = 'lsof -Fp %s' % config_file
-            out = self.send_expect(cmd, "# ", 20, alt_session)
-            if len(out):
-                lines = out.split('\r\n')
-                for line in lines:
-                    m = re.match(pid_reg, line)
-                    if m:
-                        pids.append(m.group(1))
-            for pid in pids:
-                self.send_expect('kill -9 %s' % pid, '# ', 20, alt_session)
-                self.get_session_output(timeout=2)
+            # Covers case where the process is run as a unprivileged user and does not generate the file
+            if os.path.isfile(config_file):
+                cmd = 'lsof -Fp %s' % config_file
+                out = self.send_expect(cmd, "# ", 20, alt_session)
+                if len(out):
+                    lines = out.split('\r\n')
+                    for line in lines:
+                        m = re.match(pid_reg, line)
+                        if m:
+                            pids.append(m.group(1))
+                for pid in pids:
+                    self.send_expect('kill -9 %s' % pid, '# ', 20, alt_session)
+                    self.get_session_output(timeout=2)
 
         hugepage_info = ['/var/run/dpdk/%s/hugepage_info' % file_prefix for file_prefix in prefix_list]
         for hugepage in hugepage_info:
-            cmd = 'lsof -Fp %s' % hugepage
-            out = self.send_expect(cmd, "# ", 20, alt_session)
-            if len(out) and "No such file or directory" not in out:
-                self.logger.warning("There are some dpdk process not free hugepage")
-                self.logger.warning("**************************************")
-                self.logger.warning(out)
-                self.logger.warning("**************************************")
+            # Covers case where the process is run as a unprivileged user and does not generate the file
+            if os.path.isfile(hugepage):
+                cmd = 'lsof -Fp %s' % hugepage
+                out = self.send_expect(cmd, "# ", 20, alt_session)
+                if len(out) and "No such file or directory" not in out:
+                    self.logger.warning("There are some dpdk process not free hugepage")
+                    self.logger.warning("**************************************")
+                    self.logger.warning(out)
+                    self.logger.warning("**************************************")
 
         # remove directory
         directorys = ['/var/run/dpdk/%s' % file_prefix for file_prefix in prefix_list]
diff --git a/framework/project_dpdk.py b/framework/project_dpdk.py
index 040d070..03ace46 100644
--- a/framework/project_dpdk.py
+++ b/framework/project_dpdk.py
@@ -434,7 +434,7 @@ class DPDKdut(Dut):
             current_nic += 1
 
         bind_script_path = self.get_dpdk_bind_script()
-        self.send_expect('%s --force %s' % (bind_script_path, binding_list), '# ')
+        return self.send_expect('%s --force %s' % (bind_script_path, binding_list), '# ')
 
     def unbind_interfaces_linux(self, nics_to_bind=None):
         """
diff --git a/framework/settings.py b/framework/settings.py
index f91452d..850fa89 100644
--- a/framework/settings.py
+++ b/framework/settings.py
@@ -197,6 +197,8 @@ SCAPY2IXIA = [
 
 USERNAME = 'root'
 
+# A user used to test functionality for a non-root user
+UNPRIVILEGED_USERNAME = 'dtsunprivilegedtester'
 
 """
 Helpful header sizes.
diff --git a/test_plans/linux_modules_test_plan.rst b/test_plans/linux_modules_test_plan.rst
new file mode 100644
index 0000000..c7bb2d0
--- /dev/null
+++ b/test_plans/linux_modules_test_plan.rst
@@ -0,0 +1,158 @@
+.. # BSD LICENSE
+    #
+    # Copyright(c) 2010-2014 Intel Corporation. All rights reserved.
+    # Copyright © 2018[, 2019] The University of New Hampshire. All rights reserved.
+    # All rights reserved.
+    #
+    # Redistribution and use in source and binary forms, with or without
+    # modification, are permitted provided that the following conditions
+    # are met:
+    #
+    #   * Redistributions of source code must retain the above copyright
+    #     notice, this list of conditions and the following disclaimer.
+    #   * Redistributions in binary form must reproduce the above copyright
+    #     notice, this list of conditions and the following disclaimer in
+    #     the documentation and/or other materials provided with the
+    #     distribution.
+    #   * Neither the name of Intel Corporation nor the names of its
+    #     contributors may be used to endorse or promote products derived
+    #     from this software without specific prior written permission.
+    #
+    # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+    # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+    # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+    # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+    # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+    # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+    # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+    # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+    # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+    # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+    # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+==================
+Linux Driver Tests
+==================
+
+This file contains multiple test suites to avoid a single unsupported
+kernel module causing the entire test suite to fail. These test suites
+cover a variety of kernel modules and are built to check their function
+both in use as root and as an unprivileged user. All of the test suites
+run the same tests. In the documentation for test cases, <MODULE> will
+represent the name of the module being tested. <DEV INTERFACE> will
+represent the character interface under /dev/ for the interface.
+
+Prerequisites
+=============
+
+There are two prerequisites. First, all of the drivers that you wish
+to test must be compiled and installed so that they are available through
+modprobe. Secondly, there should be a user on the dut which has the same
+password as the primary account for dts. This account will be used as the
+unprivileged user, but it still should have permission to lock at least
+1 GiB of memory to ensure that it can lock all of the process memory.
+
+Test Suites
+===========
+
+There is 1 test suite per module, the modules are as follows:
+
+    * VFIO-PCI
+    * UIO PCI GENERIC
+    * IGB UIO
+
+Test Case: TX RX
+====================
+This test case runs as root and is designed to check the basic functioning
+of the module. It checks whether packets can be sent and received.
+
+Remove old module ::
+
+    # rmmod <MODULE>
+
+Add the new one ::
+
+    # modprobe <MODULE>
+
+Bind the interface to the driver ::
+
+    # usertools/dpdk-devbind.py --force --bind=<MODULE> xxxx:xx:xx.x
+
+Start testpmd in a loop configuration ::
+
+    # x86_64-native-linux-gcc/app/testpmd  -l 1,2 -n 4 -w xxxx:xx:xx.x \
+       -- -i --port-topology=loop
+
+Start packet forwarding ::
+
+    testpmd> start
+
+Start a packet capture on the tester::
+
+    # tcpdump -i (interface) ether src (tester mac address)
+
+Send some packets to the dut and check that they are properly sent back into
+the packet capture on the tester.
+
+Test Case: TX RX Userspace
+====================
+This test case runs as the unprivileged user and is designed to check the
+basic functioning of the module. It checks whether packets can be sent
+and received when running dpdk applications as a normal user. # means
+that a command is run as root. $ means that a command is run as the user.
+The igb_uio module requires that the iova mode is in virtual address mode,
+which can be done by adding the flag "--iova-mode va" as an eal option to
+testpmd.
+
+Remove old module ::
+
+    # rmmod <MODULE>
+
+Add the new one ::
+
+    # modprobe <MODULE>
+
+Bind the interface to the driver ::
+
+    # usertools/dpdk-devbind.py --force --bind=<MODULE> xxxx:xx:xx.x
+
+Grant permissions for all users to access the new character device ::
+
+    # setfacl -m u:dtsunprivilegedtester:rwx <DEV INTERFACE>
+
+Start testpmd in a loop configuration ::
+
+    $ x86_64-native-linux-gcc/app/testpmd  -l 1,2 -n 4 -w xxxx:xx:xx.x --in-memory \
+       -- -i --port-topology=loop
+
+Start packet forwarding ::
+
+    testpmd> start
+
+Start a packet capture on the tester::
+
+    # tcpdump -i (interface) ether src (tester mac address)
+
+Send some packets to the dut and check that they are properly sent back into
+the packet capture on the tester.
+
+Test Case: Hello World
+====================
+This is a more basic test of functionality as a normal user than the
+TX RX Userspace case. It simply involves running a short, hello-world-like
+program on each core before shutting down. # means that a command is run
+as root. $ means that a command is run as the user. The igb_uio module
+requires that the iova mode is in virtual address mode, which can be done
+by adding the flag "--iova-mode va" as an eal option to the hello world
+application.
+
+Compile the application ::
+
+    # cd $RTE_SDK/examples/helloworld && make
+
+Run the application ::
+
+    $ $RTE_SDK/examples/helloworld/build/helloworld-shared --in-memory
+
+Check for any error states or reported errors.
+
diff --git a/tests/TestSuite_linux_modules.py b/tests/TestSuite_linux_modules.py
new file mode 100644
index 0000000..7cb479d
--- /dev/null
+++ b/tests/TestSuite_linux_modules.py
@@ -0,0 +1,173 @@
+# BSD LICENSE
+#
+# Copyright(c) 2010-2014 Intel Corporation. All rights reserved.
+# Copyright © 2018[, 2019] The University of New Hampshire. All rights reserved.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+#   * Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer.
+#   * Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in
+#     the documentation and/or other materials provided with the
+#     distribution.
+#   * Neither the name of Intel Corporation nor the names of its
+#     contributors may be used to endorse or promote products derived
+#     from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""
+DPDK Test suite.
+Linux Kernel Modules example.
+"""
+import os
+import time
+
+from pmd_output import PmdOutput
+from test_case import TestCase
+
+from framework import settings
+
+ETHER_HEADER_LEN = 18
+IP_HEADER_LEN = 20
+ETHER_STANDARD_MTU = 1518
+ETHER_JUMBO_FRAME_MTU = 9000
+
+
+# A class like this is used because doing inheritance normally breaks the test case discovery process
+class LinuxModulesHelperMethods:
+    driver: str
+    dev_interface: str
+    additional_eal_options: str
+
+    def set_up_all(self):
+        """
+        Prerequisite steps for each test suit.
+        """
+        self.dut_ports = self.dut.get_ports()
+        self.pmdout = PmdOutput(self.dut)
+        pci_address = self.dut.ports_info[self.dut_ports[0]]['pci']
+        self.old_driver = settings.get_nic_driver(pci_address)
+        out = self.dut.bind_interfaces_linux(driver=self.driver)
+        self.verify("bind failed" not in out, f"Failed to bind {self.driver}")
+        self.verify("not loaded" not in out, f"{self.driver} was not loaded")
+
+    def send_scapy_packet(self, port_id: int, packet: str):
+        itf = self.tester.get_interface(port_id)
+
+        self.tester.scapy_foreground()
+        self.tester.scapy_append(f'sendp({packet}, iface="{itf}")')
+        return self.tester.scapy_execute()
+
+    def tear_down(self):
+        self.dut.kill_all()
+
+    def run_example_program_in_userspace(self, directory: str, program: str):
+        """
+        A function to run a given example program as an unprivileged user.
+        @param directory: The directory under examples where the app is
+        @param program: the name of the binary to run
+        """
+        out: str = self.dut.build_dpdk_apps(f"$RTE_SDK/examples/{directory}")
+        self.verify("Error" not in out, "Compilation error")
+        self.verify("No such" not in out, "Compilation error")
+
+        program_build_location = f"$RTE_SDK/examples/{directory}/build/{program}"
+        program_user_location = f"/tmp/dut/bin/{program}"
+
+        self.dut.send_expect(f"chmod +x {program_build_location}", "# ")
+        self.dut.send_expect("mkdir -p /tmp/dut/bin/", "# ")
+        user_home_dir = self.dut.send_expect(f"cp {program_build_location} {program_user_location}", "# ")
+
+        self.dut.alt_session.send_expect(f"su {settings.UNPRIVILEGED_USERNAME}", "# ")
+        self.dut.alt_session.send_expect(f"{program_user_location} --in-memory {self.additional_eal_options}", "# ")
+        out: str = self.dut.alt_session.send_expect("echo $?", "# ")
+        self.dut.alt_session.send_expect("exit", "# ")  # Return to root session
+        self.verify(out.strip() == "0", f"{program} exited in an error state")
+
+    def tx_rx_test_helper(self, pmdout, param="", eal_param=""):
+        pmdout.start_testpmd("Default", param=f"--port-topology=loop {param}",
+                             eal_param=f"{eal_param} {self.additional_eal_options}")
+        pmdout.execute_cmd("start")
+        dut_mac = self.dut.get_mac_address(self.dut_ports[0])
+        tester_mac = self.tester.get_mac(self.tester.get_local_port(self.dut_ports[0]))
+        iface = self.tester.get_interface(self.dut_ports[0])
+        pcap_path: str = f"/tmp/tester/test-{self.driver}.pcap"
+        self.tester.send_expect(f"tcpdump -i {iface} -w /tmp/tester/test-{self.driver}.pcap ether src {tester_mac} &",
+                                "# ")
+        self.tester.send_expect(f"TCPDUMP_PID=$!", "# ")
+        self.send_scapy_packet(self.dut_ports[0],
+                               f"[Ether(dst='{dut_mac}', src='{tester_mac}')/IP()/TCP()/('a') for i in range(20)]")
+        time.sleep(.1)
+        self.tester.send_expect("kill -SIGINT $TCPDUMP_PID", "# ")
+        os.system(f"mkdir -p {settings.FOLDERS['Output']}/tmp/pcap/")
+        self.tester.session.copy_file_from(pcap_path, dst=os.path.join(settings.FOLDERS['Output'], "tmp/pcap/"))
+        out: str = self.tester.send_expect(f"tcpdump -r /tmp/tester/test-{self.driver}.pcap", "# ")
+        self.verify(len(out.splitlines()) >= 20, "Not all packets were received by the tester.")
+        pmdout.quit()
+
+    #
+    #
+    #
+    # Test cases.
+    #
+
+    def tear_down_all(self):
+        """
+        When the case of this test suite finished, the environment should
+        clear up.
+        """
+        self.dut.bind_interfaces_linux(driver=self.old_driver)
+        self.dut.kill_all()
+
+    def test_tx_rx(self):
+        """
+        Preforms the testing that needs to be done as root.
+        @param driver: The driver to test
+        """
+        self.tx_rx_test_helper(self.pmdout)
+
+    def test_helloworld(self):
+        self.run_example_program_in_userspace("helloworld", "helloworld-shared")
+
+    def test_tx_rx_userspace(self):
+        app_path = self.dut.apps_name['test-pmd']
+        self.dut.send_expect(f"chmod +rx {app_path}", "#")
+        path = self.dut.send_expect("pwd", "#")
+        self.dut.alt_session.send_expect(f"su {settings.UNPRIVILEGED_USERNAME}", "#")
+        self.dut.alt_session.send_expect(f"cd {path}", "#")
+        self.dut.send_expect(f"setfacl -m u:{settings.UNPRIVILEGED_USERNAME}:rwx {self.dev_interface}", "#")
+        self.tx_rx_test_helper(PmdOutput(self.dut, session=self.dut.alt_session), eal_param="--in-memory")
+        self.dut.alt_session.send_expect(f"exit", "#")
+
+
+class TestVfio(LinuxModulesHelperMethods, TestCase):
+    driver = "vfio-pci"
+    dev_interface = "/dev/vfio/*"
+    additional_eal_options = ""
+
+
+class TestIgbuio(LinuxModulesHelperMethods, TestCase):
+    driver = "igb_uio"
+    dev_interface = "/dev/uio*"
+    additional_eal_options = "--iova-mode va"
+
+
+class TestUioPciGeneric(LinuxModulesHelperMethods, TestCase):
+    driver = "uio_pci_generic"
+    dev_interface = "/dev/uio*"
+    additional_eal_options = ""
