4a46eb
From cfe79543cf7c96bb7598f43eabdcfd3ca011a51b Mon Sep 17 00:00:00 2001
4a46eb
From: Eduardo Otubo <otubo@redhat.com>
4a46eb
Date: Mon, 24 Aug 2020 16:03:42 -0400
4a46eb
Subject: [PATCH 3/3] DHCP sandboxing failing on noexec mounted /var/tmp (#521)
4a46eb
4a46eb
RH-Author: Eduardo Otubo <otubo@redhat.com>
4a46eb
Message-id: <20200824160342.23626-1-otubo@redhat.com>
4a46eb
Patchwork-id: 98216
4a46eb
O-Subject: [RHEL-8.2.0/RHEL-7.9/RHEL-8.2.1/RHEL-8.3.0 cloud-init PATCH] DHCP sandboxing failing on noexec mounted /var/tmp (#521)
4a46eb
Bugzilla: 1871916
4a46eb
RH-Acked-by: Cathy Avery <cavery@redhat.com>
4a46eb
RH-Acked-by: Mohammed Gamal <mgamal@redhat.com>
4a46eb
4a46eb
commit db86753f81af73826158c9522f2521f210300e2b
4a46eb
Author: Eduardo Otubo <otubo@redhat.com>
4a46eb
Date:   Mon Aug 24 15:34:24 2020 +0200
4a46eb
4a46eb
    DHCP sandboxing failing on noexec mounted /var/tmp (#521)
4a46eb
4a46eb
    * DHCP sandboxing failing on noexec mounted /var/tmp
4a46eb
4a46eb
    If /var/tmp is mounted with noexec option the DHCP sandboxing will fail
4a46eb
    with Permission Denied. This patch simply avoids this error by checking
4a46eb
    the exec permission updating the dhcp path in negative case.
4a46eb
4a46eb
    rhbz: https://bugzilla.redhat.com/show_bug.cgi?id=1857309
4a46eb
4a46eb
    Signed-off-by: Eduardo Otubo <otubo@redhat.com>
4a46eb
4a46eb
    * Replacing with os.* calls
4a46eb
4a46eb
    * Adding test and removing isfile() useless call.
4a46eb
4a46eb
    Co-authored-by: Rick Harding <rharding@mitechie.com>
4a46eb
4a46eb
Signed-off-by: Eduardo Otubo <otubo@redhat.com>
4a46eb
Signed-off-by: Jon Maloy <jmaloy.redhat.com>
4a46eb
---
4a46eb
 cloudinit/net/dhcp.py            |  6 +++++
4a46eb
 cloudinit/net/tests/test_dhcp.py | 46 ++++++++++++++++++++++++++++++++
4a46eb
 2 files changed, 52 insertions(+)
4a46eb
4a46eb
diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py
4a46eb
index c033cc8e..841e72ee 100644
4a46eb
--- a/cloudinit/net/dhcp.py
4a46eb
+++ b/cloudinit/net/dhcp.py
4a46eb
@@ -215,6 +215,12 @@ def dhcp_discovery(dhclient_cmd_path, interface, cleandir):
4a46eb
     pid_file = os.path.join(cleandir, 'dhclient.pid')
4a46eb
     lease_file = os.path.join(cleandir, 'dhcp.leases')
4a46eb
 
4a46eb
+    # In some cases files in /var/tmp may not be executable, launching dhclient
4a46eb
+    # from there will certainly raise 'Permission denied' error. Try launching
4a46eb
+    # the original dhclient instead.
4a46eb
+    if not os.access(sandbox_dhclient_cmd, os.X_OK):
4a46eb
+        sandbox_dhclient_cmd = dhclient_cmd_path
4a46eb
+
4a46eb
     # ISC dhclient needs the interface up to send initial discovery packets.
4a46eb
     # Generally dhclient relies on dhclient-script PREINIT action to bring the
4a46eb
     # link up before attempting discovery. Since we are using -sf /bin/true,
4a46eb
diff --git a/cloudinit/net/tests/test_dhcp.py b/cloudinit/net/tests/test_dhcp.py
4a46eb
index c3fa1e04..08e2cfb5 100644
4a46eb
--- a/cloudinit/net/tests/test_dhcp.py
4a46eb
+++ b/cloudinit/net/tests/test_dhcp.py
4a46eb
@@ -406,6 +406,52 @@ class TestDHCPDiscoveryClean(CiTestCase):
4a46eb
                  'eth9', '-sf', '/bin/true'], capture=True)])
4a46eb
         m_kill.assert_has_calls([mock.call(my_pid, signal.SIGKILL)])
4a46eb
 
4a46eb
+    @mock.patch('cloudinit.net.dhcp.util.get_proc_ppid')
4a46eb
+    @mock.patch('cloudinit.net.dhcp.os.kill')
4a46eb
+    @mock.patch('cloudinit.net.dhcp.subp.subp')
4a46eb
+    def test_dhcp_discovery_outside_sandbox(self, m_subp, m_kill, m_getppid):
4a46eb
+        """dhcp_discovery brings up the interface and runs dhclient.
4a46eb
+
4a46eb
+        It also returns the parsed dhcp.leases file generated in the sandbox.
4a46eb
+        """
4a46eb
+        m_subp.return_value = ('', '')
4a46eb
+        tmpdir = self.tmp_dir()
4a46eb
+        dhclient_script = os.path.join(tmpdir, 'dhclient.orig')
4a46eb
+        script_content = '#!/bin/bash\necho fake-dhclient'
4a46eb
+        write_file(dhclient_script, script_content, mode=0o755)
4a46eb
+        lease_content = dedent("""
4a46eb
+            lease {
4a46eb
+              interface "eth9";
4a46eb
+              fixed-address 192.168.2.74;
4a46eb
+              option subnet-mask 255.255.255.0;
4a46eb
+              option routers 192.168.2.1;
4a46eb
+            }
4a46eb
+        """)
4a46eb
+        lease_file = os.path.join(tmpdir, 'dhcp.leases')
4a46eb
+        write_file(lease_file, lease_content)
4a46eb
+        pid_file = os.path.join(tmpdir, 'dhclient.pid')
4a46eb
+        my_pid = 1
4a46eb
+        write_file(pid_file, "%d\n" % my_pid)
4a46eb
+        m_getppid.return_value = 1  # Indicate that dhclient has daemonized
4a46eb
+
4a46eb
+        with mock.patch('os.access', return_value=False):
4a46eb
+            self.assertCountEqual(
4a46eb
+                [{'interface': 'eth9', 'fixed-address': '192.168.2.74',
4a46eb
+                  'subnet-mask': '255.255.255.0', 'routers': '192.168.2.1'}],
4a46eb
+                dhcp_discovery(dhclient_script, 'eth9', tmpdir))
4a46eb
+        # dhclient script got copied
4a46eb
+        with open(os.path.join(tmpdir, 'dhclient.orig')) as stream:
4a46eb
+            self.assertEqual(script_content, stream.read())
4a46eb
+        # Interface was brought up before dhclient called from sandbox
4a46eb
+        m_subp.assert_has_calls([
4a46eb
+            mock.call(
4a46eb
+                ['ip', 'link', 'set', 'dev', 'eth9', 'up'], capture=True),
4a46eb
+            mock.call(
4a46eb
+                [os.path.join(tmpdir, 'dhclient.orig'), '-1', '-v', '-lf',
4a46eb
+                 lease_file, '-pf', os.path.join(tmpdir, 'dhclient.pid'),
4a46eb
+                 'eth9', '-sf', '/bin/true'], capture=True)])
4a46eb
+        m_kill.assert_has_calls([mock.call(my_pid, signal.SIGKILL)])
4a46eb
+
4a46eb
 
4a46eb
 class TestSystemdParseLeases(CiTestCase):
4a46eb
 
4a46eb
-- 
4a46eb
2.18.2
4a46eb