Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package google-osconfig-agent for
openSUSE:Factory checked in at 2026-11-10 16:29:49
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/google-osconfig-agent (Old)
and /work/SRC/openSUSE:Factory/.google-osconfig-agent.new.3759290 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "google-osconfig-agent"
Tue Nov 10 16:29:49 2026 rev:69 rq:1396509 version:20261103.02
Changes:
--------
---
/work/SRC/openSUSE:Factory/google-osconfig-agent/google-osconfig-agent.changes
2026-11-05 18:22:31.481224010 +0100
+++
/work/SRC/openSUSE:Factory/.google-osconfig-agent.new.3759290/google-osconfig-agent.changes
2026-11-10 16:30:02.823855961 +0100
@@ -1,0 +2,7 @@
+Mon Nov 9 08:33:23 UTC 2026 - John Paul Adrian Glaubitz
<[email protected]>
+
+- Add CVE-2026-79775.patch to fix github.com/diskfs/go-diskfs:
+ malformed SquashFS image can cause crashes and persistent CPU
+ loops in rclone's archive backend (bsc#1278163, CVE-2026-79775)
+
+-------------------------------------------------------------------
New:
----
CVE-2026-79775.patch
----------(New B)----------
New:
- Add CVE-2026-79775.patch to fix github.com/diskfs/go-diskfs:
malformed SquashFS image can cause crashes and persistent CPU
----------(New E)----------
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ google-osconfig-agent.spec ++++++
--- /var/tmp/diff_new_pack.p362Qy/_old 2026-11-10 16:30:03.992904859 +0100
+++ /var/tmp/diff_new_pack.p362Qy/_new 2026-11-10 16:30:03.994904943 +0100
@@ -28,6 +28,7 @@
Source0: %{shortname}-%{version}.tar.gz
Source1: vendor.tar.gz
Source2: rpmlintrc
+Patch0: CVE-2026-79775.patch
BuildRequires: golang(API) >= 1.25.5
Requires: google-guest-configs
BuildRoot: %{_tmppath}/%{name}-%{version}-build
@@ -37,6 +38,9 @@
%prep
%setup -n %{shortname}-%{version} -a1
+pushd vendor/github.com/diskfs/go-diskfs
+%patch -P0 -p1
+popd
%build
%ifnarch ppc64
++++++ CVE-2026-79775.patch ++++++
>From d9acff9930b0a13005b5c7169acadc1200122268 Mon Sep 17 00:00:00 2001
From: Yuren0312 <[email protected]>
Date: Thu, 22 Oct 2026 17:31:43 +0800
Subject: [PATCH] squashfs: reject malformed images that panic or hang the
parser
Fixes the three denial-of-service variants of GHSA-6jcg-q3wp-x2f4
(CVE-2026-79775), all reachable by parsing an untrusted image:
- a superblock block size of zero passes the block-log check
(uint16(math.Log2(0)) is 0) and then divides by zero when the
filesystem cache is sized. Hold the image's block size to the same
constraints as a caller-provided one.
- an inode or directory metadata offset that points past the end of
the uncompressed metadata block panics on the slice. Reject it.
- a metadata stream made of zero-size blocks never advances the read
loop, spinning the CPU forever on a truncated stream. Refuse blocks
that yield no data.
Each variant now returns an error instead of panicking or hanging.
Adds regression tests covering all three, including the end-to-end
Read() path.
Refs #442
---
.../ghsa_6jcg_q3wp_x2f4_internal_test.go | 189 ++++++++++++++++++
filesystem/squashfs/metadatablock.go | 12 ++
filesystem/squashfs/superblock.go | 8 +
3 files changed, 209 insertions(+)
create mode 100644 filesystem/squashfs/ghsa_6jcg_q3wp_x2f4_internal_test.go
diff --git a/filesystem/squashfs/ghsa_6jcg_q3wp_x2f4_internal_test.go
b/filesystem/squashfs/ghsa_6jcg_q3wp_x2f4_internal_test.go
new file mode 100644
index 00000000..9aeaafae
--- /dev/null
+++ b/filesystem/squashfs/ghsa_6jcg_q3wp_x2f4_internal_test.go
@@ -0,0 +1,189 @@
+package squashfs
+
+import (
+ "bytes"
+ "encoding/binary"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/diskfs/go-diskfs/backend/file"
+)
+
+// Tests guarding GHSA-6jcg-q3wp-x2f4 (CVE-2026-79775): denial of service when
+// parsing a maliciously crafted SquashFS image. The parser trusted
+// attacker-controlled superblock and metadata values in three places:
+//
+// 1. a zero block size passes the superblock block-log check
+// (uint16(math.Log2(0)) is 0) and then divides by zero when the
+// filesystem is set up;
+// 2. an inode/directory metadata offset can point past the end of the
+// uncompressed metadata block it indexes, panicking on the slice;
+// 3. a truncated metadata stream made up of zero-size blocks never advances
+// the read loop, spinning the CPU forever.
+//
+// All three are now rejected with an error instead of panicking or hanging.
+
+// testSuperblockWithBlocksize returns a valid superblock with its block size
+// and block log replaced by the given values.
+func testSuperblockWithBlocksize(b []byte, blocksize uint32, blocklog uint16)
[]byte {
+ out := append([]byte{}, b...)
+ binary.LittleEndian.PutUint32(out[12:16], blocksize)
+ binary.LittleEndian.PutUint16(out[22:24], blocklog)
+ return out
+}
+
+func TestParseSuperblockRejectsInvalidBlocksize(t *testing.T) {
+ b, _, err := testGetValidSuperblock()
+ if err != nil {
+ t.Fatal(err)
+ }
+ tests := []struct {
+ name string
+ blocksize uint32
+ blocklog uint16
+ }{
+ // uint16(math.Log2(0)) is 0, so the block-log check alone
accepts these
+ {"zero blocksize", 0, 0},
+ // too small for squashfs (minimum is 4 KiB)
+ {"too small", 2048, 11},
+ // too large for squashfs (maximum is 1 MiB)
+ {"too large", 2 * 1024 * 1024, 21},
+ // not a power of two; the log check rounds it, so it must be
caught here
+ {"not a power of two", 4097, 12},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ s, err :=
parseSuperblock(testSuperblockWithBlocksize(b, tt.blocksize, tt.blocklog))
+ if err == nil {
+ t.Fatalf("expected error for blocksize %d, got
superblock %v", tt.blocksize, s)
+ }
+ if !strings.Contains(err.Error(), "block size invalid")
{
+ t.Errorf("mismatched error, actual %v", err)
+ }
+ if s != nil {
+ t.Errorf("expected nil superblock, got %v", s)
+ }
+ })
+ }
+ // the untouched superblock must still parse
+ s, err := parseSuperblock(b)
+ if err != nil {
+ t.Fatalf("unexpected error parsing valid superblock: %v", err)
+ }
+ if s == nil {
+ t.Fatal("expected valid superblock, got nil")
+ }
+}
+
+func TestReadMetadataByteOffsetBeyondBlock(t *testing.T) {
+ // one uncompressed metadata block holding 5 bytes
+ data := []byte{0x05, 0x80, 1, 2, 3, 4, 5}
+ fs := &FileSystem{}
+ out, err := fs.readMetadata(bytes.NewReader(data), nil, 0, 0, 100, 64)
+ if err == nil {
+ t.Fatalf("expected error for byte offset 100 in a 5-byte block,
got %v", out)
+ }
+ if !strings.Contains(err.Error(), "cannot read at offset 100") {
+ t.Errorf("mismatched error, actual %v", err)
+ }
+}
+
+func TestReadMetadataZeroSizeBlocks(t *testing.T) {
+ // 512 bytes of zero-size uncompressed blocks. Every 2-byte header
reads as
+ // size 0, so the read loop never accumulates the requested bytes and,
with a
+ // compressor that tolerates empty input, spins forever reading past
the end
+ // of the data. It must fail instead.
+ data := bytes.Repeat([]byte{0x00, 0x80}, 256)
+ fs := &FileSystem{}
+ out, err := fs.readMetadata(bytes.NewReader(data),
&testCompressorAddBytes{}, 0, 0, 0, 64)
+ if err == nil {
+ t.Fatalf("expected error for zero-size metadata blocks, got
%v", out)
+ }
+ if !strings.Contains(err.Error(), "is empty") {
+ t.Errorf("mismatched error, actual %v", err)
+ }
+}
+
+func TestReadMetadata(t *testing.T) {
+ // two uncompressed metadata blocks of 5 bytes each
+ b := []byte{0x05, 0x80, 1, 2, 3, 4, 5, 0x05, 0x80, 9, 8, 7, 6, 5}
+ fs := &FileSystem{}
+ tests := []struct {
+ name string
+ b []byte
+ c Compressor
+ initialOffset uint32
+ byteOffset uint16
+ size int
+ out []byte
+ err error
+ }{
+ // single block, exact size
+ {"single block", b, nil, 0, 0, 5, []byte{1, 2, 3, 4, 5}, nil},
+ // fewer bytes than the block holds; readMetadata returns the
whole
+ // block, leaving the trimming to the caller
+ {"short read", b, nil, 0, 0, 3, []byte{1, 2, 3, 4, 5}, nil},
+ // offset into the block
+ {"offset into block", b, nil, 0, 2, 3, []byte{3, 4, 5}, nil},
+ // spanning the boundary between the two blocks
+ {"crossing blocks", b, nil, 0, 3, 7, []byte{4, 5, 9, 8, 7, 6,
5}, nil},
+ // starting in the second metadata block, via the block offset
+ {"second block", b, nil, 7, 0, 2, []byte{9, 8, 7, 6, 5}, nil},
+ // a compressed block is decompressed by the supplied compressor
+ {"compressed", []byte{0x05, 0x00, 1, 2, 3, 4, 5},
&testCompressorAddBytes{b: []byte{0x25}}, 0, 0, 6, []byte{1, 2, 3, 4, 5, 0x25},
nil},
+ // header claims more data than the block holds
+ {"truncated block", []byte{0x05, 0x80, 1, 2}, nil, 0, 0, 5,
nil, fmt.Errorf("read %d instead of expected %d bytes for metadata block at
location %d", 2, 5, 0)},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ out, err := fs.readMetadata(bytes.NewReader(tt.b),
tt.c, 0, tt.initialOffset, tt.byteOffset, tt.size)
+ switch {
+ case (err == nil && tt.err != nil) || (err != nil &&
tt.err == nil) || (err != nil && tt.err != nil &&
!strings.HasPrefix(err.Error(), tt.err.Error())):
+ t.Errorf("mismatched error, actual %v expected
%v", err, tt.err)
+ case !bytes.Equal(out, tt.out):
+ t.Errorf("mismatched output, actual % x
expected % x", out, tt.out)
+ }
+ })
+ }
+}
+
+// TestReadRejectsZeroBlocksizeSuperblock is the end-to-end regression for the
+// divide by zero: a superblock with a zero block size used to pass the
+// block-log check and panic when the filesystem cache was sized.
+func TestReadRejectsZeroBlocksizeSuperblock(t *testing.T) {
+ b := make([]byte, superblockSize)
+ binary.LittleEndian.PutUint32(b[0:4], superblockMagic)
+ binary.LittleEndian.PutUint32(b[4:8], 1) // inodes
+ binary.LittleEndian.PutUint32(b[12:16], 0)
+ binary.LittleEndian.PutUint16(b[20:22], uint16(compressionNone))
+ binary.LittleEndian.PutUint16(b[22:24], 0) // log2(0) == 0, so this
matches
+ binary.LittleEndian.PutUint16(b[24:26], 0x0200)
+ binary.LittleEndian.PutUint16(b[26:28], 0) // no ids
+ binary.LittleEndian.PutUint16(b[28:30], superblockMajorVersion)
+ binary.LittleEndian.PutUint16(b[30:32], superblockMinorVersion)
+ binary.LittleEndian.PutUint64(b[56:64], 0xffffffffffffffff) // no xattr
table
+
+ path := filepath.Join(t.TempDir(), "zero-blocksize.sqfs")
+ if err := os.WriteFile(path, b, 0o600); err != nil {
+ t.Fatal(err)
+ }
+ storage, err := file.OpenFromPath(path, true)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer storage.Close()
+
+ fs, err := Read(storage, 4096, 0, 0)
+ if err == nil {
+ t.Fatalf("expected error for zero block size, got filesystem
%v", fs != nil)
+ }
+ if !strings.Contains(err.Error(), "block size invalid") {
+ t.Errorf("mismatched error, actual %v", err)
+ }
+ if fs != nil {
+ t.Errorf("expected nil filesystem, got %v", fs)
+ }
+}
diff --git a/filesystem/squashfs/metadatablock.go
b/filesystem/squashfs/metadatablock.go
index 0436aa72..a74d6805 100644
--- a/filesystem/squashfs/metadatablock.go
+++ b/filesystem/squashfs/metadatablock.go
@@ -118,6 +118,12 @@ func (fs *FileSystem) readMetadata(r io.ReaderAt, c
Compressor, firstBlock int64
if err != nil {
return nil, err
}
+ // the offset into the uncompressed block comes from the image (e.g.
the inode
+ // reference of a directory entry) and may point past the end of the
block,
+ // which would panic when slicing
+ if int(byteOffset) > len(m) {
+ return nil, fmt.Errorf("metadata block at %d has %d bytes,
cannot read at offset %d", firstBlock+int64(blockOffset), len(m), byteOffset)
+ }
b = append(b, m[byteOffset:]...)
// do we have any more to read?
for len(b) < size {
@@ -126,6 +132,12 @@ func (fs *FileSystem) readMetadata(r io.ReaderAt, c
Compressor, firstBlock int64
if err != nil {
return nil, err
}
+ // a block that yields no data cannot advance the read. Refuse
it instead
+ // of looping forever over zero-size blocks, e.g. from a
truncated
+ // metadata stream
+ if len(m) == 0 {
+ return nil, fmt.Errorf("metadata block at %d is empty,
cannot read %d bytes of metadata", firstBlock+int64(blockOffset), size)
+ }
b = append(b, m...)
}
return b, nil
diff --git a/filesystem/squashfs/superblock.go
b/filesystem/squashfs/superblock.go
index 5fc72bf0..c86212fb 100644
--- a/filesystem/squashfs/superblock.go
+++ b/filesystem/squashfs/superblock.go
@@ -202,6 +202,14 @@ func parseSuperblock(b []byte) (*superblock, error) {
if expectedLog != blocklog {
return nil, fmt.Errorf("superblock block log mismatch, actual
%d expected %d", blocklog, expectedLog)
}
+ // the block size comes from the image, which may be untrusted, so hold
it to
+ // the same constraints as a caller-provided one. This matters because
the log
+ // check above cannot catch a zero block size: uint16(math.Log2(0)) is
0, so a
+ // zero size with a zero log passes. A zero block size then divides by
zero
+ // when the filesystem is set up.
+ if err := validateBlocksize(int64(blocksize)); err != nil {
+ return nil, fmt.Errorf("superblock block size invalid: %v", err)
+ }
flags, err := parseFlags(b[24:26])
if err != nil {
return nil, fmt.Errorf("error parsing flags bytes: %v", err)