Skip to content

Commit

Permalink
Merge pull request from GHSA-f7qw-jj9c-rpq9
Browse files Browse the repository at this point in the history
Prohibit using a differential disk as a base disk
  • Loading branch information
AkihiroSuda committed May 30, 2023
2 parents 3dbc08c + bc1bdb8 commit 01dbd4d
Show file tree
Hide file tree
Showing 5 changed files with 367 additions and 42 deletions.
133 changes: 112 additions & 21 deletions pkg/qemu/imgutil/imgutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,78 @@ import (
"encoding/json"
"fmt"
"os/exec"
"path/filepath"
"strings"

"github.com/sirupsen/logrus"
)

type InfoChild struct {
Name string `json:"name,omitempty"` // since QEMU 8.0
Info Info `json:"info,omitempty"` // since QEMU 8.0
}

type InfoFormatSpecific struct {
Type string `json:"type,omitempty"` // since QEMU 1.7
Data json.RawMessage `json:"data,omitempty"` // since QEMU 1.7
}

func (sp *InfoFormatSpecific) Qcow2() *InfoFormatSpecificDataQcow2 {
if sp.Type != "qcow2" {
return nil
}
var x InfoFormatSpecificDataQcow2
if err := json.Unmarshal(sp.Data, &x); err != nil {
panic(err)
}
return &x
}

func (sp *InfoFormatSpecific) Vmdk() *InfoFormatSpecificDataVmdk {
if sp.Type != "vmdk" {
return nil
}
var x InfoFormatSpecificDataVmdk
if err := json.Unmarshal(sp.Data, &x); err != nil {
panic(err)
}
return &x
}

type InfoFormatSpecificDataQcow2 struct {
Compat string `json:"compat,omitempty"` // since QEMU 1.7
LazyRefcounts bool `json:"lazy-refcounts,omitempty"` // since QEMU 1.7
Corrupt bool `json:"corrupt,omitempty"` // since QEMU 2.2
RefcountBits int `json:"refcount-bits,omitempty"` // since QEMU 2.3
CompressionType string `json:"compression-type,omitempty"` // since QEMU 5.1
ExtendedL2 bool `json:"extended-l2,omitempty"` // since QEMU 5.2
}

type InfoFormatSpecificDataVmdk struct {
CreateType string `json:"create-type,omitempty"` // since QEMU 1.7
CID int `json:"cid,omitempty"` // since QEMU 1.7
ParentCID int `json:"parent-cid,omitempty"` // since QEMU 1.7
Extents []InfoFormatSpecificDataVmdkExtent `json:"extents,omitempty"` // since QEMU 1.7
}

type InfoFormatSpecificDataVmdkExtent struct {
Filename string `json:"filename,omitempty"` // since QEMU 1.7
Format string `json:"format,omitempty"` // since QEMU 1.7
VSize int64 `json:"virtual-size,omitempty"` // since QEMU 1.7
ClusterSize int `json:"cluster-size,omitempty"` // since QEMU 1.7
}

// Info corresponds to the output of `qemu-img info --output=json FILE`
type Info struct {
Format string `json:"format,omitempty"` // since QEMU 1.3
VSize int64 `json:"virtual-size,omitempty"`
Filename string `json:"filename,omitempty"` // since QEMU 1.3
Format string `json:"format,omitempty"` // since QEMU 1.3
VSize int64 `json:"virtual-size,omitempty"` // since QEMU 1.3
ActualSize int64 `json:"actual-size,omitempty"` // since QEMU 1.3
DirtyFlag bool `json:"dirty-flag,omitempty"` // since QEMU 5.2
ClusterSize int `json:"cluster-size,omitempty"` // since QEMU 1.3
BackingFilename string `json:"backing-filename,omitempty"` // since QEMU 1.3
FullBackingFilename string `json:"full-backing-filename,omitempty"` // since QEMU 1.3
BackingFilenameFormat string `json:"backing-filename-format,omitempty"` // since QEMU 1.3
FormatSpecific *InfoFormatSpecific `json:"format-specific,omitempty"` // since QEMU 1.7
Children []InfoChild `json:"children,omitempty"` // since QEMU 8.0
}

func ConvertToRaw(source string, dest string) error {
Expand All @@ -27,6 +91,14 @@ func ConvertToRaw(source string, dest string) error {
return nil
}

func ParseInfo(b []byte) (*Info, error) {
var imgInfo Info
if err := json.Unmarshal(b, &imgInfo); err != nil {
return nil, err
}
return &imgInfo, nil
}

func GetInfo(f string) (*Info, error) {
var stdout, stderr bytes.Buffer
cmd := exec.Command("qemu-img", "info", "--output=json", "--force-share", f)
Expand All @@ -36,26 +108,45 @@ func GetInfo(f string) (*Info, error) {
return nil, fmt.Errorf("failed to run %v: stdout=%q, stderr=%q: %w",
cmd.Args, stdout.String(), stderr.String(), err)
}
var imgInfo Info
if err := json.Unmarshal(stdout.Bytes(), &imgInfo); err != nil {
return nil, err
}
return &imgInfo, nil
return ParseInfo(stdout.Bytes())
}

func DetectFormat(f string) (string, error) {
switch ext := strings.ToLower(filepath.Ext(f)); ext {
case ".qcow2":
return "qcow2", nil
case ".raw":
return "raw", nil
func AcceptableAsBasedisk(info *Info) error {
switch info.Format {
case "qcow2", "raw":
// NOP
default:
logrus.WithField("filename", info.Filename).
Warnf("Unsupported image format %q. The image may not boot, or may have an extra privilege to access the host filesystem. Use with caution.", info.Format)
}
if info.BackingFilename != "" {
return fmt.Errorf("base disk (%q) must not have a backing file (%q)", info.Filename, info.BackingFilename)
}
imgInfo, err := GetInfo(f)
if err != nil {
return "", err
if info.FullBackingFilename != "" {
return fmt.Errorf("base disk (%q) must not have a backing file (%q)", info.Filename, info.FullBackingFilename)
}
if imgInfo.Format == "" {
return "", fmt.Errorf("failed to detect format of %q", f)
if info.FormatSpecific != nil {
if vmdk := info.FormatSpecific.Vmdk(); vmdk != nil {
for _, e := range vmdk.Extents {
if e.Filename != info.Filename {
return fmt.Errorf("base disk (%q) must not have an extent file (%q)", info.Filename, e.Filename)
}
}
}
}
return imgInfo.Format, nil
// info.Children is set since QEMU 8.0
switch len(info.Children) {
case 0:
// NOP
case 1:
if info.Filename != info.Children[0].Info.Filename {
return fmt.Errorf("base disk (%q) child must not have a different filename (%q)", info.Filename, info.Children[0].Info.Filename)
}
if len(info.Children[0].Info.Children) > 0 {
return fmt.Errorf("base disk (%q) child must not have children of its own", info.Filename)
}
default:
return fmt.Errorf("base disk (%q) must not have multiple children: %+v", info.Filename, info.Children)
}
return nil
}
212 changes: 212 additions & 0 deletions pkg/qemu/imgutil/imgutil_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package imgutil

import (
"testing"

"gotest.tools/v3/assert"
)

func TestParseInfo(t *testing.T) {
t.Run("qcow2", func(t *testing.T) {
// qemu-img create -f qcow2 foo.qcow2 4G
// (QEMU 8.0)
const s = `{
"children": [
{
"name": "file",
"info": {
"children": [
],
"virtual-size": 197120,
"filename": "foo.qcow2",
"format": "file",
"actual-size": 200704,
"format-specific": {
"type": "file",
"data": {
}
},
"dirty-flag": false
}
}
],
"virtual-size": 4294967296,
"filename": "foo.qcow2",
"cluster-size": 65536,
"format": "qcow2",
"actual-size": 200704,
"format-specific": {
"type": "qcow2",
"data": {
"compat": "1.1",
"compression-type": "zlib",
"lazy-refcounts": false,
"refcount-bits": 16,
"corrupt": false,
"extended-l2": false
}
},
"dirty-flag": false
}`

info, err := ParseInfo([]byte(s))
assert.NilError(t, err)
assert.Equal(t, 1, len(info.Children))
assert.Check(t, info.FormatSpecific != nil)
qcow2 := info.FormatSpecific.Qcow2()
assert.Check(t, qcow2 != nil)
assert.Equal(t, qcow2.Compat, "1.1")

t.Run("diff", func(t *testing.T) {
// qemu-img create -f qcow2 -F qcow2 -b foo.qcow2 bar.qcow2
// (QEMU 8.0)
const s = `{
"children": [
{
"name": "file",
"info": {
"children": [
],
"virtual-size": 197120,
"filename": "bar.qcow2",
"format": "file",
"actual-size": 200704,
"format-specific": {
"type": "file",
"data": {
}
},
"dirty-flag": false
}
}
],
"backing-filename-format": "qcow2",
"virtual-size": 4294967296,
"filename": "bar.qcow2",
"cluster-size": 65536,
"format": "qcow2",
"actual-size": 200704,
"format-specific": {
"type": "qcow2",
"data": {
"compat": "1.1",
"compression-type": "zlib",
"lazy-refcounts": false,
"refcount-bits": 16,
"corrupt": false,
"extended-l2": false
}
},
"full-backing-filename": "foo.qcow2",
"backing-filename": "foo.qcow2",
"dirty-flag": false
}`
info, err := ParseInfo([]byte(s))
assert.NilError(t, err)
assert.Equal(t, 1, len(info.Children))
assert.Equal(t, "foo.qcow2", info.BackingFilename)
assert.Equal(t, "bar.qcow2", info.Filename)
assert.Check(t, info.FormatSpecific != nil)
qcow2 := info.FormatSpecific.Qcow2()
assert.Check(t, qcow2 != nil)
assert.Equal(t, qcow2.Compat, "1.1")
})
})
t.Run("vmdk", func(t *testing.T) {
t.Run("twoGbMaxExtentSparse", func(t *testing.T) {
// qemu-img create -f vmdk foo.vmdk 4G -o subformat=twoGbMaxExtentSparse
// (QEMU 8.0)
const s = `{
"children": [
{
"name": "extents.1",
"info": {
"children": [
],
"virtual-size": 327680,
"filename": "foo-s002.vmdk",
"format": "file",
"actual-size": 327680,
"format-specific": {
"type": "file",
"data": {
}
},
"dirty-flag": false
}
},
{
"name": "extents.0",
"info": {
"children": [
],
"virtual-size": 327680,
"filename": "foo-s001.vmdk",
"format": "file",
"actual-size": 327680,
"format-specific": {
"type": "file",
"data": {
}
},
"dirty-flag": false
}
},
{
"name": "file",
"info": {
"children": [
],
"virtual-size": 512,
"filename": "foo.vmdk",
"format": "file",
"actual-size": 4096,
"format-specific": {
"type": "file",
"data": {
}
},
"dirty-flag": false
}
}
],
"virtual-size": 4294967296,
"filename": "foo.vmdk",
"cluster-size": 65536,
"format": "vmdk",
"actual-size": 659456,
"format-specific": {
"type": "vmdk",
"data": {
"cid": 918420663,
"parent-cid": 4294967295,
"create-type": "twoGbMaxExtentSparse",
"extents": [
{
"virtual-size": 2147483648,
"filename": "foo-s001.vmdk",
"cluster-size": 65536,
"format": "SPARSE"
},
{
"virtual-size": 2147483648,
"filename": "foo-s002.vmdk",
"cluster-size": 65536,
"format": "SPARSE"
}
]
}
},
"dirty-flag": false
}`
info, err := ParseInfo([]byte(s))
assert.NilError(t, err)
assert.Equal(t, 3, len(info.Children))
assert.Equal(t, "foo.vmdk", info.Filename)
assert.Check(t, info.FormatSpecific != nil)
vmdk := info.FormatSpecific.Vmdk()
assert.Check(t, vmdk != nil)
assert.Equal(t, vmdk.CreateType, "twoGbMaxExtentSparse")
})
})
}
Loading

0 comments on commit 01dbd4d

Please sign in to comment.