Skip to content
This repository has been archived by the owner on Oct 2, 2024. It is now read-only.

make --force=seccomp the default behavior #1720

Merged
merged 11 commits into from
Sep 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions bin/ch-image.py.in
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,8 @@ def main():
help="set build-time variable ARG to VAL, or $ARG if no VAL")
sp.add_argument("-f", "--file", metavar="DOCKERFILE",
help="Dockerfile to use (default: CONTEXT/Dockerfile)")
sp.add_argument("--force", metavar="MODE", nargs="?", default=None,
choices=["fakeroot", "seccomp"], const="seccomp",
sp.add_argument("--force", metavar="MODE", nargs="?", default="seccomp",
type=ch.Force_Mode, const="seccomp",
help="inject unprivileged build workarounds")
sp.add_argument("--force-cmd", metavar="CMD,ARG1[,ARG2...]",
action="append", default=[],
Expand Down
46 changes: 28 additions & 18 deletions doc/ch-image.rst
Original file line number Diff line number Diff line change
Expand Up @@ -559,9 +559,10 @@ Options:
this case.

:code:`--force[=MODE]`
Use unprivileged build workarounds of mode :code:`MODE`, which can be
:code:`fakeroot` or :code:`seccomp` (the default). See section “Privilege
model” below for details on what this does and when you might need it.
Use unprivileged build with root emulation mode :code:`MODE`, which can be
:code:`fakeroot`, :code:`seccomp` (the default), or :code:`none`. See
section “Privilege model” below for details on what this does and when you
might need it.

:code:`--force-cmd=CMD,ARG1[,ARG2...]`
If command :code:`CMD` is found in a :code:`RUN` instruction, add the
Expand Down Expand Up @@ -612,20 +613,18 @@ or “`fakeroot <https://sylabs.io/guides/3.7/user-guide/fakeroot.html>`_” mod
of some competing builders, which do require privileged supporting code or
utilities.

Without workarounds provided by :code:`--force`, this approach does confuse
programs that expect to have real root privileges, most notably distribution
package installers. This subsection describes why that happens and what you
can do about it.
Without root emulation, this approach does confuse programs that expect to have
real root privileges, most notably distribution package installers. This
subsection describes why that happens and what you can do about it.

:code:`ch-image` executes all instructions as the normal user who invokes it.
For :code:`RUN`, this is accomplished with :code:`ch-run` arguments including
:code:`-w --uid=0 --gid=0`. That is, your host EUID and EGID are both mapped
to zero inside the container, and only one UID (zero) and GID (zero) are
available inside the container. Under this arrangement, processes running in
the container for each :code:`RUN` *appear* to be running as root, but many
privileged system calls will fail without the workarounds described below.
**This affects any fully unprivileged container build, not just
Charliecloud.**
:code:`-w --uid=0 --gid=0`. That is, your host EUID and EGID are both mapped to
zero inside the container, and only one UID (zero) and GID (zero) are available
inside the container. Under this arrangement, processes running in the container
for each :code:`RUN` *appear* to be running as root, but many privileged system
calls will fail without the root emulation methods described below. **This
affects any fully unprivileged container build, not just Charliecloud.**

The most common time to see this is installing packages. For example, here is
RPM failing to :code:`chown(2)` a file, which makes the package update fail:
Expand All @@ -651,8 +650,8 @@ Charliecloud provides two different mechanisms to avoid these problems. Both
involve lying to the containerized process about privileged system calls, but
at very different levels of complexity.

Workaround mode :code:`fakeroot`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Root emulation mode :code:`fakeroot`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

This mode uses :code:`fakeroot(1)` to maintain an elaborate web of deceit that
is internally consistent. This program intercepts both privileged system calls
Expand Down Expand Up @@ -700,8 +699,8 @@ exactly what it is doing.
:code:`fakeroot` mode works and :code:`seccomp` does not, please let us
know.

Workaround mode :code:`seccomp` (default)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Root emulation mode :code:`seccomp` (default)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

This mode uses the kernel’s :code:`seccomp(2)` system call filtering to
intercept certain privileged system calls, do absolutely nothing, and return
Expand Down Expand Up @@ -742,6 +741,17 @@ filter and has no knowledge of which instructions actually used the
intercepted system calls. Therefore, the printed “instructions modified”
number is only a count of instructions with a hook applied as described above.

:code:`RUN` instruction
~~~~~~~~~~~~~~~~~~~~~~~

In terminal output, image metadata, and the build cache, the :code:`RUN`
instruction is always logged as :code:`RUN.S`, :code:`RUN.F`, or :code:`RUN.N`.
The letter appended to the instruction reflects the root emulation mode used
during the build in which the instruction was executed. :code:`RUN.S` indicates
:code:`seccomp`, :code:`RUN.F` indicates :code:`fakeroot`, and :code:`RUN.N`
indicates that neither form of root emulation was used (:code:`--force=none`).


Compatibility with other Dockerfile interpreters
------------------------------------------------

Expand Down
25 changes: 0 additions & 25 deletions doc/faq.rst
Original file line number Diff line number Diff line change
Expand Up @@ -99,31 +99,6 @@ two solutions:
processes and one writes a file in the image that another is reading or
writing).

:code:`ch-image` fails with "argument --force: invalid choice"
--------------------------------------------------------------

This happens when specifying the context directly after the :code:`--force`
option, e.g.

::

$ ch-image build --force examples/hello/
[...]
ch-image build: error: argument --force: invalid choice: 'examples/hello/' (choose from 'fakeroot', 'seccomp')

This happens because the command line interprets the argument after
:code:`--force` as the optional input for :code:`--force`. When said argument
isn’t :code:`fakeroot` or :code:`seccomp`, the program throws an error. The
solution is to add a :code:`--` after :code:`--force` to indicate the end of
the command line options, e.g.

::

$ ch-image build --force -- .
inferred image name: hello
[...]
grown in 3 instructions: hello

:code:`ch-image` fails with "certificate verify failed"
-------------------------------------------------------

Expand Down
12 changes: 5 additions & 7 deletions doc/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ should be able to tell you if this is linked in.
::

$ cd /usr/local/share/doc/charliecloud/examples/hello
$ ch-image build --force -- .
$ ch-image build .
inferred image name: hello
[...]
grown in 3 instructions: hello
Expand All @@ -67,7 +67,7 @@ section.
::

$ cd /usr/local/share/doc/charliecloud/examples/hello
$ ch-image build --force .
$ ch-image build .
inferred image name: hello
[...]
grown in 4 instructions: hello
Expand Down Expand Up @@ -444,16 +444,14 @@ These two build should take about 15 minutes total, depending on the speed of
your system.

Note that Charliecloud infers their names from the Dockerfile name, so we
don’t need to specify :code:`-t`. Also, :code:`--force` enables some
workarounds for tools like distribution package managers that expect to really
be root.
don’t need to specify :code:`-t`.

::

$ ch-image build --force \
$ ch-image build \
-f /usr/local/share/doc/charliecloud/examples/Dockerfile.almalinux_8ch \
/usr/local/share/doc/charliecloud/examples
$ ch-image build --force \
$ ch-image build \
-f /usr/local/share/doc/charliecloud/examples/Dockerfile.openmpi \
/usr/local/share/doc/charliecloud/examples

Expand Down
20 changes: 10 additions & 10 deletions lib/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,23 +88,23 @@ def main(cli_):
ch.INFO("inferred image name: %s" % cli.tag)

# --force and friends.
if (cli.force_cmd and cli.force == "fakeroot"):
if (cli.force_cmd and cli.force == ch.Force_Mode.FAKEROOT):
ch.FATAL("--force-cmd and --force=fakeroot are incompatible")
if (not cli.force_cmd):
cli.force_cmd = force.FORCE_CMD_DEFAULT
else:
cli.force = "seccomp"
cli.force = ch.Force_Mode.SECCOMP
# convert cli.force_cmd to parsed dict
force_cmd = dict()
for line in cli.force_cmd:
(cmd, args) = force.force_cmd_parse(line)
force_cmd[cmd] = args
cli.force_cmd = force_cmd
ch.VERBOSE("force mode: %s" % cli.force)
if (cli.force == "seccomp"):
if (cli.force == ch.Force_Mode.SECCOMP):
for (cmd, args) in cli.force_cmd.items():
ch.VERBOSE("force command: %s" % ch.argv_to_string([cmd] + args))
if ( cli.force == "seccomp"
if ( cli.force == ch.Force_Mode.SECCOMP
and ch.cmd([ch.CH_BIN + "/ch-run", "--feature=seccomp"],
fail_ok=True) != 0):
ch.FATAL("ch-run was not built with seccomp(2) support")
Expand Down Expand Up @@ -199,9 +199,9 @@ def build_arg_get(arg):
if (ml.instruction_total_ct == 0):
ch.FATAL("no instructions found: %s" % cli.file)
assert (ml.inst_prev.image_i + 1 == image_ct) # should’ve errored already
if (cli.force and ml.miss_ct != 0):
if ((cli.force != ch.Force_Mode.NONE) and ml.miss_ct != 0):
ch.INFO("--force=%s: modified %d RUN instructions"
% (cli.force, forcer.run_modified_ct))
% (cli.force.value, forcer.run_modified_ct))
ch.INFO("grown in %d instructions: %s"
% (ml.instruction_total_ct, ml.inst_prev.image))
# FIXME: remove when we’re done encouraging people to use the build cache.
Expand Down Expand Up @@ -1155,17 +1155,17 @@ class Run(Instruction):
def str_name(self):
# Can’t get this from the forcer object because it might not have been
# initialized yet.
if (cli.force is None):
tag = ""
elif (cli.force == "fakeroot"):
if (cli.force == ch.Force_Mode.NONE):
tag = ".N"
elif (cli.force == ch.Force_Mode.FAKEROOT):
# FIXME: This causes spurious misses because it adds the force tag to
# *all* RUN instructions, not just those that actually were modified
# (i.e, any RUN instruction will miss the equivalent RUN without
# --force=fakeroot). But we don’t know know if an instruction needs
# modifications until the result is checked out, which happens after
# we check the cache. See issue #1339.
tag = ".F"
elif (cli.force == "seccomp"):
elif (cli.force == ch.Force_Mode.SECCOMP):
tag = ".S"
else:
assert False, "unreachable code reached"
Expand Down
6 changes: 6 additions & 0 deletions lib/charliecloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ class Download_Mode(enum.Enum):
ENABLED = "enabled"
WRITE_ONLY = "write-only"

# Root emulation mode
class Force_Mode(enum.Enum):
FAKEROOT="fakeroot"
SECCOMP="seccomp"
NONE="none"


## Constants ##

Expand Down
6 changes: 3 additions & 3 deletions lib/force.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,11 +292,11 @@
def new(image_path, force_mode, force_cmds):
"""Return a new forcer object appropriate for image at image_path in mode
force_mode. If no such object can be found, exit with error."""
if (force_mode is None):
if (force_mode == ch.Force_Mode.NONE):
return Nope()
elif (force_mode == "fakeroot"):
elif (force_mode == ch.Force_Mode.FAKEROOT):
return Fakeroot(image_path)
elif (force_mode == "seccomp"):
elif (force_mode == ch.Force_Mode.SECCOMP):
return Seccomp(force_cmds)
else:
assert False, "unreachable code reached"
Expand Down
6 changes: 3 additions & 3 deletions test/approved-trailing-whitespace
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@
./test/build/50_dockerfile.bats:93:RUN echo test4 \
./test/build/50_dockerfile.bats:96:RUN echo test4 \
./test/build/50_dockerfile.bats:97:b \
./test/build/50_dockerfile.bats:131: 4. RUN true
./test/build/50_dockerfile.bats:433:#ENV chse_1a value 1a
./test/build/50_dockerfile.bats:436:#ENV chse_1c=value\ 1c\
./test/build/50_dockerfile.bats:131: 4. RUN.S true
lucaudill marked this conversation as resolved.
Show resolved Hide resolved
./test/build/50_dockerfile.bats:434:#ENV chse_1a value 1a
./test/build/50_dockerfile.bats:437:#ENV chse_1c=value\ 1c\
18 changes: 9 additions & 9 deletions test/build/50_ch-image.bats
Original file line number Diff line number Diff line change
Expand Up @@ -615,39 +615,39 @@ EOF
},
{
"created": "2021-11-30T20:40:24Z",
"created_by": "RUN echo \"cwd1: $PWD\""
"created_by": "RUN.S echo \"cwd1: $PWD\""
},
{
"created": "2021-11-30T20:40:24Z",
"created_by": "WORKDIR /usr"
},
{
"created": "2021-11-30T20:40:24Z",
"created_by": "RUN echo \"cwd2: $PWD\""
"created_by": "RUN.S echo \"cwd2: $PWD\""
},
{
"created": "2021-11-30T20:40:24Z",
"created_by": "RUN env | egrep '^(PATH=|ch_)' | sed -E 's/^/env1: /' | sort"
"created_by": "RUN.S env | egrep '^(PATH=|ch_)' | sed -E 's/^/env1: /' | sort"
},
{
"created": "2021-11-30T20:40:24Z",
"created_by": "ENV ch_baz='baz-ev'"
},
{
"created": "2021-11-30T20:40:24Z",
"created_by": "RUN env | egrep '^(PATH=|ch_)' | sed -E 's/^/env2: /' | sort"
"created_by": "RUN.S env | egrep '^(PATH=|ch_)' | sed -E 's/^/env2: /' | sort"
},
{
"created": "2021-11-30T20:40:25Z",
"created_by": "RUN echo \"shell1: $0\""
"created_by": "RUN.S echo \"shell1: $0\""
},
{
"created": "2021-11-30T20:40:25Z",
"created_by": "SHELL ['/bin/sh', '-v', '-c']"
},
{
"created": "2021-11-30T20:40:25Z",
"created_by": "RUN echo \"shell2: $0\""
"created_by": "RUN.S echo \"shell2: $0\""
}
],
"labels": {
Expand Down Expand Up @@ -878,21 +878,21 @@ EOF
echo "$output"
[[ $status -eq 0 ]]
[[ $output = *'1* FROM alpine:3.17'* ]]
[[ $output = *'2. RUN true'* ]]
[[ $output = *'2. RUN.S true'* ]]

echo
echo '*** Build again: hit'
run ch-image build -t tmpimg -f "$df1" "$BATS_TMPDIR"
echo "$output"
[[ $status -eq 0 ]]
[[ $output = *'1* FROM alpine:3.17'* ]]
[[ $output = *'2* RUN true'* ]]
[[ $output = *'2* RUN.S true'* ]]

echo
echo '*** Build a 3rd time with the second base image: should now miss'
run ch-image build -t tmpimg -f "$df2" "$BATS_TMPDIR"
echo "$output"
[[ $status -eq 0 ]]
[[ $output = *'1* FROM alpine:3.16'* ]]
[[ $output = *'2. RUN true'* ]]
[[ $output = *'2. RUN.S true'* ]]
}
Loading