On Fri, May 21, 2021 at 3:30 PM Moritz Muehlenhoff <j...@debian.org> wrote:
> Package: golang-github-containers-image > Severity: important > Tags: security > X-Debbugs-Cc: Debian Security Team <t...@security.debian.org> > > This was assigned CVE-2021-20291: > > https://github.com/containers/storage/commit/306fcabc964470e4b3b87a43a8f6b7d698209ee1 > > > Moritz, here is some more context on severity impact of this issue: https://bugzilla.redhat.com/show_bug.cgi?id=1939485#c23 > For applications like podman the affect is minimal - podman pull and it seemingly waits to download the image forever, cancel and the affect is negated. For something like crio the affect is more severe, with the malicious image locking the service up, BUT it is still somewhat responsive. The service then must be killed before returning back to normal. The referenced commit changes the containers/storage code to no longer fork()/exec() out to the xz executable, but instead use the golang-native implementation of golang-github-ulikunitz-xz-dev, which addresses the deadlock situation by avoiding awkward unix process coordination. Upstream switched to version 0.5.10, whereas we only have 0.5.6 in Debian. That version is at least susceptible against: - https://github.com/ulikunitz/xz/issues/35 - CVE-2020-16845 - https://github.com/ulikunitz/xz/commit/69c6093c7b2397b923acf82cb378f55ab2652b9b (similar to/same as CVE-2020-16845) - https://github.com/ulikunitz/xz/issues/40 -- no CVE, but could also lead to a DoS situation, I guess Given the limited impact of this issue (it could leave podman hanging, leading to a DoS situation in some scenarios), the absence of any unit tests, and the fact that we'd need to rebuild podman and friends anyways, I'm pondering whether making this change is worth the risk. Moritz, what do you think? If we decided to proceed, the debdiff would look like this: diff --git a/debian/changelog b/debian/changelog index 837efeeb1..ad17e4867 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +golang-github-containers-storage (1.24.8+dfsg1-2) unstable; urgency=high + + * Build against system copy of golang-github-ulikunitz-xz-dev, + Adresses: CVE-2021-20291, Closes: #988942 + + -- Reinhard Tartler <siret...@tauware.de> Fri, 21 May 2021 16:04:46 -0400 + golang-github-containers-storage (1.24.8+dfsg1-1) unstable; urgency=medium * New upstream release, focused on targetted bugfixes for podman 3.0 diff --git a/debian/control b/debian/control index 086dbcb3d..c5c362961 100644 --- a/debian/control +++ b/debian/control @@ -24,6 +24,7 @@ Build-Depends: debhelper-compat (= 11), golang-github-pquerna-ffjson-dev, golang-github-sirupsen-logrus-dev, golang-github-stretchr-testify-dev, + golang-github-ulikunitz-xz-dev, golang-github-vbatts-tar-split-dev, golang-go (>> 2:1.14~~), golang-go-patricia-dev, diff --git a/debian/patches/CVE-2021-20291.patch b/debian/patches/CVE-2021-20291.patch new file mode 100644 index 000000000..f87427443 --- /dev/null +++ b/debian/patches/CVE-2021-20291.patch @@ -0,0 +1,212 @@ +From 306fcabc964470e4b3b87a43a8f6b7d698209ee1 Mon Sep 17 00:00:00 2001 +From: Nalin Dahyabhai <na...@redhat.com> +Date: Wed, 17 Mar 2021 17:24:14 -0400 +Subject: [PATCH] Use an xz library instead of shelling out to xz for + decompression + +When decompressing layers compressed with xz, use a library rather than +shelling out to the xz CLI. + +Signed-off-by: Nalin Dahyabhai <na...@redhat.com> + +--- a/go.mod ++++ b/go.mod +@@ -23,6 +23,7 @@ + github.com/stretchr/testify v1.6.1 + github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2 + github.com/tchap/go-patricia v2.3.0+incompatible ++ github.com/ulikunitz/xz v0.5.10 + github.com/vbatts/tar-split v0.11.1 + golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 + golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3 +--- a/pkg/archive/archive.go ++++ b/pkg/archive/archive.go +@@ -9,7 +9,6 @@ + "io" + "io/ioutil" + "os" +- "os/exec" + "path/filepath" + "runtime" + "strings" +@@ -18,7 +17,6 @@ + + "github.com/containers/storage/pkg/fileutils" + "github.com/containers/storage/pkg/idtools" +- "github.com/containers/storage/pkg/ioutils" + "github.com/containers/storage/pkg/pools" + "github.com/containers/storage/pkg/promise" + "github.com/containers/storage/pkg/system" +@@ -26,6 +24,7 @@ + rsystem "github.com/opencontainers/runc/libcontainer/system" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" ++ "github.com/ulikunitz/xz" + ) + + type ( +@@ -167,12 +166,6 @@ + return Uncompressed + } + +-func xzDecompress(archive io.Reader) (io.ReadCloser, <-chan struct{}, error) { +- args := []string{"xz", "-d", "-c", "-q"} +- +- return cmdStream(exec.Command(args[0], args[1:]...), archive) +-} +- + // DecompressStream decompresses the archive and returns a ReaderCloser with the decompressed archive. + func DecompressStream(archive io.Reader) (io.ReadCloser, error) { + p := pools.BufioReader32KPool +@@ -205,15 +198,12 @@ + readBufWrapper := p.NewReadCloserWrapper(buf, bz2Reader) + return readBufWrapper, nil + case Xz: +- xzReader, chdone, err := xzDecompress(buf) ++ xzReader, err := xz.NewReader(buf) + if err != nil { + return nil, err + } + readBufWrapper := p.NewReadCloserWrapper(buf, xzReader) +- return ioutils.NewReadCloserWrapper(readBufWrapper, func() error { +- <-chdone +- return readBufWrapper.Close() +- }), nil ++ return readBufWrapper, nil + case Zstd: + return zstdReader(buf) + default: +@@ -1306,35 +1296,6 @@ + return nil + } + +-// cmdStream executes a command, and returns its stdout as a stream. +-// If the command fails to run or doesn't complete successfully, an error +-// will be returned, including anything written on stderr. +-func cmdStream(cmd *exec.Cmd, input io.Reader) (io.ReadCloser, <-chan struct{}, error) { +- chdone := make(chan struct{}) +- cmd.Stdin = input +- pipeR, pipeW := io.Pipe() +- cmd.Stdout = pipeW +- var errBuf bytes.Buffer +- cmd.Stderr = &errBuf +- +- // Run the command and return the pipe +- if err := cmd.Start(); err != nil { +- return nil, nil, err +- } +- +- // Copy stdout to the returned pipe +- go func() { +- if err := cmd.Wait(); err != nil { +- pipeW.CloseWithError(fmt.Errorf("%s: %s", err, errBuf.String())) +- } else { +- pipeW.Close() +- } +- close(chdone) +- }() +- +- return pipeR, chdone, nil +-} +- + // NewTempArchive reads the content of src into a temporary file, and returns the contents + // of that file as an archive. The archive can only be read once - as soon as reading completes, + // the file will be deleted. +--- a/pkg/archive/archive_test.go ++++ b/pkg/archive/archive_test.go +@@ -12,7 +12,6 @@ + "runtime" + "strings" + "testing" +- "time" + + "github.com/containers/storage/pkg/idtools" + "github.com/stretchr/testify/assert" +@@ -209,59 +208,6 @@ + } + } + +-func TestCmdStreamLargeStderr(t *testing.T) { +- cmd := exec.Command("sh", "-c", "dd if=/dev/zero bs=1k count=1000 of=/dev/stderr; echo hello") +- out, _, err := cmdStream(cmd, nil) +- if err != nil { +- t.Fatalf("Failed to start command: %s", err) +- } +- errCh := make(chan error) +- go func() { +- _, err := io.Copy(ioutil.Discard, out) +- errCh <- err +- }() +- select { +- case err := <-errCh: +- if err != nil { +- t.Fatalf("Command should not have failed (err=%.100s...)", err) +- } +- case <-time.After(5 * time.Second): +- t.Fatalf("Command did not complete in 5 seconds; probable deadlock") +- } +-} +- +-func TestCmdStreamBad(t *testing.T) { +- // TODO Windows: Figure out why this is failing in CI but not locally +- if runtime.GOOS == windows { +- t.Skip("Failing on Windows CI machines") +- } +- badCmd := exec.Command("sh", "-c", "echo hello; echo >&2 error couldn\\'t reverse the phase pulser; exit 1") +- out, _, err := cmdStream(badCmd, nil) +- if err != nil { +- t.Fatalf("Failed to start command: %s", err) +- } +- if output, err := ioutil.ReadAll(out); err == nil { +- t.Fatalf("Command should have failed") +- } else if err.Error() != "exit status 1: error couldn't reverse the phase pulser\n" { +- t.Fatalf("Wrong error value (%s)", err) +- } else if s := string(output); s != "hello\n" { +- t.Fatalf("Command output should be '%s', not '%s'", "hello\\n", output) +- } +-} +- +-func TestCmdStreamGood(t *testing.T) { +- cmd := exec.Command("sh", "-c", "echo hello; exit 0") +- out, _, err := cmdStream(cmd, nil) +- if err != nil { +- t.Fatal(err) +- } +- if output, err := ioutil.ReadAll(out); err != nil { +- t.Fatalf("Command should not have failed (err=%s)", err) +- } else if s := string(output); s != "hello\n" { +- t.Fatalf("Command output should be '%s', not '%s'", "hello\\n", output) +- } +-} +- + func TestUntarPathWithInvalidDest(t *testing.T) { + tempFolder, err := ioutil.TempDir("", "storage-archive-test") + require.NoError(t, err) +--- /dev/null ++++ b/tests/apply-junk.bats +@@ -0,0 +1,25 @@ ++#!/usr/bin/env bats ++ ++load helpers ++ ++@test "applyjunk" { ++ # Create and try to populate layers with... garbage. It should be ++ # rejected cleanly. ++ for compressed in cat gzip bzip2 xz zstd ; do ++ storage create-layer --id layer-${compressed} ++ ++ echo [[${compressed} /etc/os-release]] ++ ${compressed} < /etc/os-release > junkfile ++ run storage apply-diff --file junkfile layer-${compressed} ++ echo "$output" ++ [[ "$status" -ne 0 ]] ++ [[ "$output" =~ "invalid tar header" ]] || [[ "$output" =~ "unexpected EOF" ]] ++ ++ echo [[${compressed}]] ++ echo "sorry, not even enough info for a tar header here" | ${compressed} > junkfile ++ run storage apply-diff --file junkfile layer-${compressed} ++ echo "$output" ++ [[ "$status" -ne 0 ]] ++ [[ "$output" =~ "unexpected EOF" ]] ++ done ++} diff --git a/debian/patches/series b/debian/patches/series index d802103b9..2b57c2636 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -1 +1,2 @@ test.patch +CVE-2021-20291.patch regards, Reinhard