Repository: tern-tools/tern (language: python)

Issue:
Enable Tern to run in an unprivileged container
Fixes #1086

This PR brings in changes required to enable Tern to run in an
unprivileged container. This allows us to run Tern on platforms
that do not allow containers with CAP_SYS_ADMIN privileges.

The major change is to include a method of applying the diff
filesystem layers using bulk copying of the files and directories
into the merge directory, rather than using a storage driver like
overlay2 or fuse. However, in order to do this, we also need to
manually handle whiteout files. Commits 1 and 2 handle
detection and use of whiteout files. Commit 1 reverts a change
that excludes whiteout files during the tarball extraction and Commit
2 handles checking and tracking of whiteout files in each image
layer.

Commit 3 removes any unnecessary mounting of inodes, thus
removing the need to run any kind of bind mounts. Commit 4
introduces the manual application of layers to replace overlay
mounting. Commit 5 handles applying layers when there is a
cache hit. Finally Commit 6 modifies the Dockerfiles to no
longer run tern with an external driver. The option is still available
via the `--driver fuse` option, hence not breaking operation for
downstream users. At the same time, the docker_run.sh script
will use the default driver rather than the fuse driver.

Signed-off-by: Nisha K <nishak@vmware.com>
Feature: run tern in a container without using --unprivileged
**Describe the Feature**
In order to allow Tern to run on kubernetes, it needs to run in an unprivileged container. Hence our current use of `docker run --privileged` is not acceptable.

**Implementation Changes**
1. Create a "driver" that does not require mounting
2. Create a method of applying layers without using overlay or fuse
3. Move overlay2 mounting to it's own `--driver` option



Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index 168d8fe7..ce1eb1c8 100644
--- a/README.md
+++ b/README.md
@@ -47,10 +47,10 @@ Tern is a software package inspection tool that can create a Software Bill of Ma
 
 # What is Tern?<a name="what-is-tern">
 Tern is an inspection tool to find the metadata of the packages installed in a container image. The overall operation looks like this:
-1. It uses overlayfs to mount the first filesystem layer (also known as the BaseOS) used to build the container image
-2. It then executes scripts from the "command library" in a chroot environment to collect information about packages installed in that layer
-3. With that information as a starting point, it continues to iterate over steps 1 and 2 for the rest of the layers in the container image
-4. Once done, it generates a report, various format options are available. The report, in its default format, provides a verbose, layer by layer, explanation of the various software components imported. If a Dockerfile is provided, the report indicates the Dockerfile lines corresponding to each of the file system layers.
+1. It analyzes the first layer of the container image to collect information like distro type, package format, and package managers.
+2. It then executes scripts from the "command library" in a chroot environment to collect information about packages installed in that layer.
+3. With that information as a starting point, it continues to analyze the subsequent layers in the container image.
+4. Once done, it generates a report of packages with their metadata. Several formats are available. The report, in its default format, provides a layer by layer, explanation of the various software components imported. If a Dockerfile is provided, the report indicates the Dockerfile lines corresponding to each of the file system layers.
 
 Tern gives you a deeper understanding of your container's bill of materials so you can make better decisions about your container based infrastructure, integration and deployment strategies. It's also a good tool if you are curious about the contents of the container images you have built.
 
@@ -115,13 +115,21 @@ Build the Docker image (called `ternd` here). You may need to use sudo:
 $ docker build -f docker/Dockerfile -t ternd .
 ```
 
+This will install the latest release of tern using pip.
+
+If you want to build a Docker image containing the latest changes to tern, run:
+```
+$ python setup.py sdist
+$ docker build -f ci/Dockerfile -t ternd .
+```
+
 **NOTE**: By default, Tern will run with logging turned on. If you would like to silent the terminal output when running the ternd container, make the following change to the Dockerfile ENTRYPOINT before building:
 
 ```
 --- a/Dockerfile
 +++ b/Dockerfile
--ENTRYPOINT ["tern", "--driver", "fuse"]
-+ENTRYPOINT ["tern", "-q", "--driver", "fuse"]
+-ENTRYPOINT ["tern"]
++ENTRYPOINT ["tern", "-q"]
 ```
 
 Run the script `docker_run.sh`. You may need to use sudo. In the below command `debian` is the docker hub container image name  and `buster` is the tag that identifies the version we are interested in analyzing.
@@ -135,12 +143,15 @@ To produce a json report run
 $ ./docker_run.sh ternd "report -f json -i debian:buster"
 ```
 
-What the `docker_run.sh` script does is run the built container as privileged.
-
-*WARNING:* privileged Docker containers are not secure. DO NOT run this container in production unless you have secured the node (VM or bare metal machine) that the docker daemon is running on.
+What the `docker_run.sh` script does is run the built container.
 
 Tern is not distributed as Docker images yet. This is coming soon. Watch the [Project Status](#project-status) for updates.
 
+**WARNING**: If using the `--driver fuse` or `--driver overlay2` storage driver options, then the docker image needs to run as privileged.
+```
+docker run --privileged -v /var/run/docker.sock:/var/run/docker.sock ternd "--driver fuse report -i debian:buster"
+```
+
 ## Getting Started with Vagrant<a name="getting-started-with-vagrant">
 Vagrant is a tool to setup an isolated virtual software development environment. If you are using Windows or Mac OSes and want to run Tern from the command line (not in a Docker container) this is the best way to get started as Tern does not run natively in a Mac OS or Windows environment at this time.
 
diff --git a/ci/Dockerfile b/ci/Dockerfile
index 2e553a7f..454fb831 100644
--- a/ci/Dockerfile
+++ b/ci/Dockerfile
@@ -1,39 +1,32 @@
-# Copyright (c) 2019-2020 VMware, Inc. All Rights Reserved.
+# Copyright (c) 2019-2021 VMware, Inc. All Rights Reserved.
 # SPDX-License-Identifier: BSD-2-Clause
 
-FROM debian:buster
+FROM python:3.9-slim-buster as base
 
-# Install fuse-overlayfs and Tern dependencies
-RUN apt-get update && \
-    apt-get -y install \
+FROM base as builder
+
+RUN mkdir /install
+WORKDIR /install
+
+COPY dist/tern-*.tar.gz .
+RUN pip install --no-warn-script-location --prefix=/install \
+    tern-*.tar.gz
+
+FROM base
+
+RUN echo "deb http://deb.debian.org/debian bullseye main" > /etc/apt/sources.list.d/bullseye.list \
+    && echo "Package: *\nPin: release n=bullseye\nPin-Priority: 50" > /etc/apt/preferences.d/bullseye \
+    && apt-get update \
+    && apt-get install -y --no-install-recommends \
     attr \
     findutils \
+    fuse-overlayfs/bullseye \
+    fuse3/bullseye \
     git \
-    gnupg2 \
     jq \
-    python3 \
-    python3-pip \
-    python3-setuptools \
-    tar \
-    util-linux \
-    wget && \
-    echo 'deb http://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/Debian_10/ /' > /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list && \
-    wget --no-verbose https://download.opensuse.org/repositories/devel:kubic:libcontainers:stable/Debian_10/Release.key -O - | apt-key add - && \
-    apt-get update && \
-    apt-get -y install \
-    buildah \
-    fuse-overlayfs && \
-    apt-get clean && \
-    rm -rf /var/lib/apt/lists/*
-
-# Adjust storage.conf to enable Fuse storage.
-RUN sed -i -e 's|^#mount_program|mount_program|g' -e '/additionalimage.*/a "/var/lib/shared",' /etc/containers/storage.conf
-
-# Install tern with latest changes
-COPY dist/tern-*.tar.gz .
-RUN pip3 install --upgrade pip && \
-    pip3 install --no-cache-dir \
-    tern-*.tar.gz
+    && rm -rf /var/lib/apt/lists/*
+
+COPY --from=builder /install /usr/local
 
-ENTRYPOINT ["tern", "--driver", "fuse"]
-CMD ["-h"]
+ENTRYPOINT ["tern"]
+CMD ["--help"]
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 9fe6fc15..4ef1d87b 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -1,4 +1,4 @@
-# Copyright (c) 2019-2020 VMware, Inc. All Rights Reserved.
+# Copyright (c) 2019-2021 VMware, Inc. All Rights Reserved.
 # SPDX-License-Identifier: BSD-2-Clause
 
 FROM python:3.9-slim-buster as base
@@ -27,5 +27,5 @@ RUN echo "deb http://deb.debian.org/debian bullseye main" > /etc/apt/sources.lis
 
 COPY --from=builder /install /usr/local
 
-ENTRYPOINT ["tern", "--driver", "fuse"]
-CMD ["--help"]
\ No newline at end of file
+ENTRYPOINT ["tern"]
+CMD ["--help"]
diff --git a/docker/Dockerfile.scancode b/docker/Dockerfile.scancode
index 2760638a..4ede3df6 100644
--- a/docker/Dockerfile.scancode
+++ b/docker/Dockerfile.scancode
@@ -1,4 +1,4 @@
-# Copyright (c) 2019-2020 VMware, Inc. All Rights Reserved.
+# Copyright (c) 2019-2021 VMware, Inc. All Rights Reserved.
 # SPDX-License-Identifier: BSD-2-Clause
 
 FROM python:3.9-slim-buster as base
@@ -34,5 +34,5 @@ RUN echo "deb http://deb.debian.org/debian bullseye main" > /etc/apt/sources.lis
 
 COPY --from=builder /install /usr/local
 
-ENTRYPOINT ["tern", "--driver", "fuse"]
+ENTRYPOINT ["tern"]
 CMD ["--help"]
diff --git a/docker_run.sh b/docker_run.sh
index 25428d27..92a164ec 100755
--- a/docker_run.sh
+++ b/docker_run.sh
@@ -1,6 +1,6 @@
 #!/bin/sh
 #
-# Copyright (c) 2019-2020 VMware, Inc. All Rights Reserved.
+# Copyright (c) 2019-2021 VMware, Inc. All Rights Reserved.
 # SPDX-License-Identifier: BSD-2-Clause
 #
 # Script to run Tern within a prebuilt Docker container
@@ -11,4 +11,4 @@
 # Usage: ./docker_run.sh <tern image> <tern command arguments in quotes> > output.txt
 # Example: ./docker_run.sh ternd "report -i golang:alpine" > output.txt
 
-docker run --privileged --device /dev/fuse -v /var/run/docker.sock:/var/run/docker.sock --rm $1 $2
+docker run -v /var/run/docker.sock:/var/run/docker.sock --rm $1 $2
diff --git a/tern/__main__.py b/tern/__main__.py
index 64e8467e..8f93e1ab 100755
--- a/tern/__main__.py
+++ b/tern/__main__.py
@@ -145,11 +145,12 @@ def main():
                         help="Change default working directory to specified"
                         " absolute path.")
     parser.add_argument('-dr', '--driver', metavar="DRIVER_OPTION",
-                        help="Required when running Tern in a container."
-                        "Using 'fuse' will enable the fuse-overlayfs driver "
-                        "to mount the diff layers of the container. If no "
-                        "input is provided, 'fuse' will be used as the "
-                        "default option.")
+                        default='default',
+                        help="Choose from the following storage drivers: \n"
+                        "overlay2: Use the kernel's overlay2 storage driver\n"
+                        "fuse: Use the fuse-overlayfs system tool\n"
+                        "If no option is given, the default method of "
+                        "applying container layers in userspace will be used.")
 
     # sys.version gives more information than we care to print
     py_ver = sys.version.replace('\n', '').split('[', maxsplit=1)[0]
diff --git a/tern/analyze/default/container/multi_layer.py b/tern/analyze/default/container/multi_layer.py
index f24d3307..10b98726 100644
--- a/tern/analyze/default/container/multi_layer.py
+++ b/tern/analyze/default/container/multi_layer.py
@@ -7,7 +7,9 @@
 Functions to analyze the subsequent layers in default mode
 """
 
+import glob
 import logging
+import os
 
 from tern.report import errors
 from tern.utils import constants
@@ -25,7 +27,7 @@
 logger = logging.getLogger(constants.logger_name)
 
 
-def mount_overlay_fs(image_obj, top_layer, driver=None):
+def mount_overlay_fs(image_obj, top_layer, driver):
     '''Given the image object and the top most layer, mount all the layers
     until the top layer using overlayfs'''
     tar_layers = []
@@ -35,6 +37,40 @@ def mount_overlay_fs(image_obj, top_layer, driver=None):
     return target
 
 
+def apply_layers(image_obj, top_layer):
+    """Apply image diff layers without using a kernel snapshot driver"""
+    # All merging happens in the merge directory
+    target = os.path.join(rootfs.get_working_dir(), constants.mergedir)
+    layer_dir = rootfs.get_untar_dir(image_obj.layers[top_layer].tar_file)
+    layer_contents = layer_dir + '/*'
+    # Account for whiteout files
+    for fd in image_obj.layers[top_layer].files:
+        if fd.is_whiteout:
+            # delete the corresponding file or directory in the target
+            # directory as well as the layer contents directory
+            deleted = fd.name.replace('.wh.', '')
+            delpath = os.path.join(target, os.path.dirname(fd.path), deleted)
+            if os.path.exists(delpath):
+                if os.path.isfile(delpath):
+                    rootfs.root_command(['rm'], delpath)
+                else:
+                    rootfs.root_command(rootfs.remove, delpath)
+                os.remove(os.path.join(layer_dir, fd.path))
+    # Finally, bulk copy the layer contents into the target directory
+    # if there are any files to move
+    if os.listdir(layer_dir):
+        rootfs.root_command(['cp', '-r'] + glob.glob(layer_contents), target)
+    return target
+
+
+def prep_layers(image_obj, top_layer, driver='default'):
+    """Prepare the layer diff contents uptil the required point in time
+    based on the driver"""
+    if driver == 'default':
+        return apply_layers(image_obj, top_layer)
+    return mount_overlay_fs(image_obj, top_layer, driver)
+
+
 def fresh_analysis(image_obj, curr_layer, prereqs, options):
     """This is a subroutine that is run if there is no chached results or if
     the user wants to redo the analysis
@@ -53,12 +89,10 @@ def fresh_analysis(image_obj, curr_layer, prereqs, options):
     # if there is no shell, try to see if it exists in the current layer
     if not prereqs.fs_shell:
         prereqs.fs_shell = dcom.get_shell(image_obj.layers[curr_layer])
-    # mount diff layers from 0 till the current layer
-    target = mount_overlay_fs(image_obj, curr_layer, options.driver)
+    # prep layers based on the chosen driver
+    target = prep_layers(image_obj, curr_layer, options.driver)
     # set this layer's host path
     prereqs.host_path = target
-    # mount dev, sys and proc after mounting diff layers
-    rootfs.prep_rootfs(target)
     # get commands that created the layer
     # for docker images this is retrieved from the image history
     command_list = dcom.get_commands_from_metadata(
@@ -78,8 +112,9 @@ def fresh_analysis(image_obj, curr_layer, prereqs, options):
     else:
         # fall back to executing what we know
         core.execute_base(image_obj.layers[curr_layer], prereqs)
-    rootfs.undo_mount()
-    rootfs.unmount_rootfs()
+    # if the driver is using some storage driver, unmount the rootfs
+    if options.driver != 'default':
+        rootfs.unmount_rootfs()
 
 
 def analyze_subsequent_layers(image_obj, prereqs, master_list, options):
@@ -92,6 +127,10 @@ def analyze_subsequent_layers(image_obj, prereqs, master_list, options):
         fresh analysis
         package information and bundle it into the image object
         3. Update the master list"""
+    # if the driver is 'default' then the first layer needs to be extracted
+    mergepath = os.path.join(rootfs.get_working_dir(), constants.mergedir)
+    if not os.listdir(mergepath):
+        prep_layers(image_obj, 0, 'default')
     curr_layer = 1
     # get list of environment variables
     prereqs.envs = lock.get_env_vars(image_obj)
@@ -115,6 +154,9 @@ def analyze_subsequent_layers(image_obj, prereqs, master_list, options):
         if not common.load_from_cache(image_obj.layers[curr_layer],
                                       options.redo):
             fresh_analysis(image_obj, curr_layer, prereqs, options)
+        # If the driver is 'default' apply the current layer anyway
+        if options.driver == 'default':
+            apply_layers(image_obj, curr_layer)
         # update the master list
         dcom.update_master_list(master_list, image_obj.layers[curr_layer])
         curr_layer = curr_layer + 1
diff --git a/tern/analyze/default/container/single_layer.py b/tern/analyze/default/container/single_layer.py
index a21ed0d4..aa4aac66 100644
--- a/tern/analyze/default/container/single_layer.py
+++ b/tern/analyze/default/container/single_layer.py
@@ -98,13 +98,13 @@ def get_os_style(image_layer, binary):
     image_layer.pkg_format = command_lib.check_pkg_format(binary)
 
 
-def mount_first_layer(layer_obj):
+def prep_first_layer(layer_obj):
     try:
-        target = rootfs.mount_base_layer(layer_obj.tar_file)
+        target = rootfs.prep_base_layer(layer_obj.tar_file)
         rootfs.prep_rootfs(target)
         return target
     except subprocess.CalledProcessError as e:  # nosec
-        logger.critical("Cannot mount filesystem and/or device nodes: %s", e)
+        logger.critical("Cannot create device nodes: %s", e)
         dcom.abort_analysis()
         return None
     except KeyboardInterrupt:
@@ -155,15 +155,12 @@ def analyze_first_layer(image_obj, master_list, options):
         # if there is a binary, extract packages
         if prereqs.binary:
             # mount the first layer
-            target_dir = mount_first_layer(image_obj.layers[0])
+            target_dir = prep_first_layer(image_obj.layers[0])
             # set the host path to the mount point
             if target_dir:
                 prereqs.host_path = target_dir
             # core default execution on the first layer
             core.execute_base(image_obj.layers[0], prereqs)
-            # unmount
-            rootfs.undo_mount()
-            rootfs.unmount_rootfs()
         else:
             logger.warning(errors.no_package_manager)
             image_obj.layers[0].origins.add_notice_to_origins(
diff --git a/tern/analyze/default/debug/run.py b/tern/analyze/default/debug/run.py
index b5b3949b..5bbdeb84 100644
--- a/tern/analyze/default/debug/run.py
+++ b/tern/analyze/default/debug/run.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (c) 2017-2020 VMware, Inc. All Rights Reserved.
+# Copyright (c) 2017-2021 VMware, Inc. All Rights Reserved.
 # SPDX-License-Identifier: BSD-2-Clause
 
 """
@@ -41,11 +41,11 @@ def check_image_obj(image_string):
 def mount_container_image(image_obj, driver=None):
     """Mount the container image to make it ready to invoke scripts"""
     if len(image_obj.layers) > 1:
-        target = multi_layer.mount_overlay_fs(
+        target = multi_layer.prep_layers(
             image_obj, len(image_obj.layers) - 1, driver)
         rootfs.prep_rootfs(target)
     else:
-        single_layer.mount_first_layer(image_obj.layers[0])
+        single_layer.prep_first_layer(image_obj.layers[0])
 
 
 def look_up_lib(keys):
@@ -106,12 +106,10 @@ def drop_into_layer(image_obj, layer_index):
     upto the specified layer index and drop into a shell session"""
     rootfs.set_up()
     if layer_index == 0:
-        # mount only one layer
-        target = rootfs.mount_base_layer(
-            image_obj.layers[layer_index].tar_file)
+        target = rootfs.prep_base_layer(image_obj.layers[layer_index].tar_file)
     else:
         # mount all layers uptil the provided layer index
-        target = multi_layer.mount_overlay_fs(image_obj, layer_index)
+        target = multi_layer.prep_layers(image_obj, layer_index)
     mount_path = get_mount_path()
     print("\nWorking directory is: {}\n".format(mount_path))
     # check if there is a shell
@@ -138,7 +136,6 @@ def execute_invoke(image_obj, args):
     # invoke commands in chroot
     invoke_script(args)
     # undo the mounts
-    rootfs.undo_mount()
     rootfs.unmount_rootfs()
     # cleanup
     rootfs.clean_up()
@@ -179,10 +176,6 @@ def execute_step(image_obj, args):
 
 def recover():
     """Undo all the mounts and clean up directories"""
-    try:
-        rootfs.undo_mount()
-    except subprocess.CalledProcessError:
-        pass
     try:
         rootfs.unmount_rootfs()
     except subprocess.CalledProcessError:
diff --git a/tern/classes/file_data.py b/tern/classes/file_data.py
index d20e1d2b..3ce595e3 100644
--- a/tern/classes/file_data.py
+++ b/tern/classes/file_data.py
@@ -39,6 +39,7 @@ class FileData:
         urls: a list of urls from where this file could come from
         checksums: a dictionary of the form {<checksum_type>: <checksum>,...}
         checksum types and checksums are stored in lower case
+        is_whiteout: True if the file is a whiteout file
 
     methods:
         to_dict: returns a dictionary representation of the instance
@@ -71,6 +72,7 @@ def __init__(self,
         self.urls = []
         self.__checksums = {}
         self.__origins = Origins()
+        self.__is_whiteout = False
 
     @property
     def name(self):
@@ -150,6 +152,10 @@ def short_file_type(self, short_file_type):
     def origins(self):
         return self.__origins
 
+    @property
+    def is_whiteout(self):
+        return self.__is_whiteout
+
     def set_checksum(self, checksum_type='', checksum=''):
         '''Set the checksum type and checksum of the file'''
         # TODO: calculate the checksum if not given
@@ -271,3 +277,9 @@ def is_equal(self, other):
             return False
         return (self.name == other.name and self.path == other.path and
                 self.checksum == other.checksum)
+
+    def set_whiteout(self):
+        """Based on the file prefix, set whether the file is
+        a whiteout file"""
+        if '.wh.' in self.name:
+            self.__is_whiteout = True
diff --git a/tern/classes/image_layer.py b/tern/classes/image_layer.py
index 3f5728a3..2108a02c 100644
--- a/tern/classes/image_layer.py
+++ b/tern/classes/image_layer.py
@@ -312,21 +312,18 @@ def add_files(self):
         fs_path = rootfs.get_untar_dir(self.__tar_file)
         hash_file = os.path.join(os.path.dirname(fs_path),
                                  self.__fs_hash) + '.txt'
-        pattern = re.compile(r'([\w\-|]+)\s+(.+)')
         with open(hash_file, encoding='utf-8') as f:
             content = f.readlines()
         for line in content:
-            m = pattern.search(line)
-            if m:
-                # m.group(2) contains the file path
-                # m.group(1) contains the extattrs and checksum
-                file_data = FileData(os.path.basename(m.group(2)),
-                                     os.path.relpath(m.group(2), '.'))
-                # attrs_tuple contains (extattrs, '|', checksum)
-                attrs_tuple = m.group(1).rpartition('|')
-                file_data.set_checksum('sha256', attrs_tuple[2])
-                file_data.extattrs = attrs_tuple[0]
-                self.add_file(file_data)
+            # lines are of the form:
+            # permissions|uid|gid|size|hard links|  sha256sum  filepath
+            file_info = line[:-1].split('  ')
+            file_data = FileData(os.path.basename(file_info[2]),
+                                 os.path.relpath(file_info[2], '.'))
+            file_data.set_checksum('sha256', file_info[1])
+            file_data.extattrs = file_info[0]
+            file_data.set_whiteout()
+            self.add_file(file_data)
 
     def get_layer_workdir(self):
         # If the layer is created by a WORKDIR command then return the workdir
diff --git a/tern/prep.py b/tern/prep.py
index cff81cd1..6b96148a 100644
--- a/tern/prep.py
+++ b/tern/prep.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (c) 2017-2020 VMware, Inc. All Rights Reserved.
+# Copyright (c) 2017-2021 VMware, Inc. All Rights Reserved.
 # SPDX-License-Identifier: BSD-2-Clause
 
 """
@@ -9,6 +9,7 @@
 
 import logging
 import os
+import pkg_resources
 import shutil
 
 from tern.utils import constants
@@ -32,6 +33,9 @@ def setup(working_dir=None):
     rootfs.set_working_dir(working_dir)
     # load the cache
     cache.load()
+    # required to run in a container natively on Windows
+    fs_hash_path = pkg_resources.resource_filename("tern", "tools/fs_hash.sh")
+    rootfs.root_command(["chmod", "+x", fs_hash_path])
 
 
 def teardown(keep=False):
diff --git a/tern/tools/fs_hash.sh b/tern/tools/fs_hash.sh
index 6f4ef58f..cc943c5c 100755
--- a/tern/tools/fs_hash.sh
+++ b/tern/tools/fs_hash.sh
@@ -1,6 +1,6 @@
 #!/bin/sh
 #
-# Copyright (c) 2018-2019 VMware, Inc. All Rights Reserved.
+# Copyright (c) 2018-2021 VMware, Inc. All Rights Reserved.
 # SPDX-License-Identifier: BSD-2-Clause
 #
 # Given a file path, create a list of file stats and their sha256sums
@@ -14,11 +14,11 @@
 # Check that all commands to collect metadata exist on the
 # system otherwise exit.
 
-command -v find || { echo "'find' not found on system." >&2 ; exit 1; }
-command -v sha256sum || { echo "'sha256sum' not found on system." >&2 ; exit 1; }
-command -v getfattr || { echo "'getfattr' not found on system." >&2 ; exit 1; }
+command -v find > /dev/null || { echo "'find' not found on system." >&2 ; exit 1; }
+command -v sha256sum > /dev/null || { echo "'sha256sum' not found on system." >&2 ; exit 1; }
+command -v getfattr > /dev/null || { echo "'getfattr' not found on system." >&2 ; exit 1; }
 
 cwd=`pwd`
 cd $1
-find -type f ! -size 0 -printf "%M|%U|%G|%s|%n|" -exec sha256sum {} \; -exec getfattr -d -m - {} \;
+find -type f -printf "%M|%U|%G|%s|%n|  " -exec sha256sum {} \; -exec getfattr -d -m - {} \;
 cd $cwd
diff --git a/tern/utils/rootfs.py b/tern/utils/rootfs.py
index eb1e13c9..a162d89a 100644
--- a/tern/utils/rootfs.py
+++ b/tern/utils/rootfs.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 #
-# Copyright (c) 2017-2020 VMware, Inc. All Rights Reserved.
+# Copyright (c) 2017-2021 VMware, Inc. All Rights Reserved.
 # SPDX-License-Identifier: BSD-2-Clause
 
 """
@@ -21,25 +21,20 @@
 
 # tar commands
 check_tar = ['tar', '-tf']
-extract_tar = ['tar', '-x', '--exclude=.wh.*', '-f']
-
-# mount commands
-mount = ['mount', '-o', 'bind']
-mount_proc = ['mount', '-t', 'proc', '/proc']
-mount_sys = ['mount', '-o', 'bind', '/sys']
-mount_dev = ['mount', '-o', 'bind', '/dev']
-unmount = ['umount']
+extract_tar = ['tar', '-x', '-f']
 
 working_dir = None
 
 # enable host DNS settings
 host_dns = ['cp', constants.resolv_path]
 
-# unshare PID within rootfs
-unshare_pid = ['unshare', '-pf']
+# chroot command
+chroot = ['chroot']
 
 # union mount
 union_mount = ['mount', '-t', 'overlay', 'overlay', '-o']
+# unmount union mounts
+unmount = ['umount']
 
 # fuse-overlayfs mount
 fuse_mount = ['fuse-overlayfs', '-o']
@@ -178,21 +173,19 @@ def extract_tarfile(tar_path, directory_path):
 
 
 def prep_rootfs(rootfs_dir):
-    '''Mount required filesystems in the rootfs directory'''
+    """Prepare the rootfs for chroot operation"""
+    # rpm style OSs require a /dev/urandom inode
+    # rpm also requires a pre-configured resolv.conf - which we will
+    # copy from the host
     rootfs_path = os.path.abspath(rootfs_dir)
-    proc_path = os.path.join(rootfs_path, 'proc')
-    sys_path = os.path.join(rootfs_path, 'sys')
     dev_path = os.path.join(rootfs_path, 'dev')
-    if not os.path.exists(proc_path):
-        os.mkdir(proc_path)
-    if not os.path.exists(sys_path):
-        os.mkdir(sys_path)
     if not os.path.exists(dev_path):
         os.mkdir(dev_path)
+    urandom = os.path.join(dev_path, 'urandom')
     try:
-        root_command(mount_proc, os.path.join(rootfs_path, 'proc'))
-        root_command(mount_sys, os.path.join(rootfs_path, 'sys'))
-        root_command(mount_dev, os.path.join(rootfs_path, 'dev'))
+        if not os.path.exists(urandom):
+            root_command(
+                ['mknod', os.path.join(dev_path, 'urandom'), 'c', '1', '9'])
         root_command(host_dns, os.path.join(
             rootfs_path, constants.resolv_path[1:]))
     except subprocess.CalledProcessError as error:
@@ -200,17 +193,18 @@ def prep_rootfs(rootfs_dir):
         raise
 
 
-def mount_base_layer(base_layer_tar):
-    '''To mount to base layer:
-        1. Untar the base layer tar file
-        2. Mount into mergedir'''
-    base_rootfs_path = get_untar_dir(base_layer_tar)
+def prep_base_layer(base_layer_tar):
+    """Given the name of an image's base layer's tar file, prepare the
+    merge directory with the tar file's contents:
+        1. Untar the base layer contents into the merge directory
+        2. Prepare it for chroot operation"""
+    tar_path = os.path.join(get_working_dir(), base_layer_tar)
     target_dir_path = os.path.join(get_working_dir(), constants.mergedir)
-    root_command(mount, base_rootfs_path, target_dir_path)
+    extract_tarfile(tar_path, target_dir_path)
     return target_dir_path
 
 
-def mount_diff_layers(diff_layers_tar, driver=None):
+def mount_diff_layers(diff_layers_tar, driver='overlay2'):
     '''Using overlayfs, mount all the layer tarballs'''
     # make a list of directory paths to give to lowerdir
     lower_dir_paths = []
@@ -224,7 +218,7 @@ def mount_diff_layers(diff_layers_tar, driver=None):
            ',workdir=' + workdir_path
     if driver == 'fuse':
         root_command(fuse_mount, args, merge_dir_path)
-    else:
+    if driver == 'overlay2':
         root_command(union_mount, args, merge_dir_path)
     return merge_dir_path
 
@@ -232,11 +226,8 @@ def mount_diff_layers(diff_layers_tar, driver=None):
 def run_chroot_command(command_string, shell):
     '''Run the command string in a chroot jail within the rootfs namespace'''
     target_dir = os.path.join(get_working_dir(), constants.mergedir)
-    mount_proc = '--mount-proc=' + os.path.join(
-        os.path.abspath(target_dir), 'proc')
     try:
-        result = root_command(unshare_pid, mount_proc, 'chroot', target_dir,
-                              shell, '-c', command_string)
+        result = root_command(chroot, target_dir, shell, '-c', command_string)
         return result
     except subprocess.CalledProcessError as e:
         logger.warning("Error executing command in chroot")
@@ -255,14 +246,6 @@ def run_host_command(command_string, shell):
             1, cmd=command_string, output=None, stderr=e.stderr)
 
 
-def undo_mount():
-    '''Unmount proc, sys, and dev directories'''
-    rootfs_path = os.path.join(get_working_dir(), constants.mergedir)
-    root_command(unmount, os.path.join(rootfs_path, 'proc'))
-    root_command(unmount, os.path.join(rootfs_path, 'sys'))
-    root_command(unmount, os.path.join(rootfs_path, 'dev'))
-
-
 def unmount_rootfs():
     '''Unmount the filesystem'''
     rootfs_path = os.path.join(get_working_dir(), constants.mergedir)
@@ -280,10 +263,6 @@ def clean_up():
 def recover():
     '''Recover after some external error'''
     # try to unmount proc, sys and dev first
-    try:
-        undo_mount()
-    except subprocess.CalledProcessError:
-        pass
     try:
         unmount_rootfs()
     except subprocess.CalledProcessError:
@@ -302,8 +281,6 @@ def calc_fs_hash(fs_path):
     try:
         fs_hash_path = pkg_resources.resource_filename(
             "tern", "tools/fs_hash.sh")
-        # required to run in a container natively on Windows
-        root_command(["chmod", "+x", fs_hash_path])
         hash_contents = root_command(
             [fs_hash_path], os.path.abspath(fs_path))
         file_name = hashlib.sha256(hash_contents).hexdigest()


Test patch:
diff --git a/tests/test_class_file_data.py b/tests/test_class_file_data.py
index cc4c5195..5e7ca449 100644
--- a/tests/test_class_file_data.py
+++ b/tests/test_class_file_data.py
@@ -195,6 +195,13 @@ def testMerge(self):
         self.assertEqual(
             file1.origins.origins[0].notices[0].message, 'something happened')
 
+    def testSetWhiteout(self):
+        whiteout = FileData('.wh.os-release', 'usr/lib/.wh.os-release')
+        whiteout.set_whiteout()
+        self.assertTrue(whiteout.is_whiteout)
+        self.afile.set_whiteout()
+        self.assertFalse(self.afile.is_whiteout)
+
 
 if __name__ == '__main__':
     unittest.main()