Skip to content

Commit 01dbd4d

Browse files
authored
Merge pull request from GHSA-f7qw-jj9c-rpq9
Prohibit using a differential disk as a base disk
2 parents 3dbc08c + bc1bdb8 commit 01dbd4d

File tree

5 files changed

+367
-42
lines changed

5 files changed

+367
-42
lines changed

pkg/qemu/imgutil/imgutil.go

Lines changed: 112 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,78 @@ import (
55
"encoding/json"
66
"fmt"
77
"os/exec"
8-
"path/filepath"
9-
"strings"
8+
9+
"github.com/sirupsen/logrus"
1010
)
1111

12+
type InfoChild struct {
13+
Name string `json:"name,omitempty"` // since QEMU 8.0
14+
Info Info `json:"info,omitempty"` // since QEMU 8.0
15+
}
16+
17+
type InfoFormatSpecific struct {
18+
Type string `json:"type,omitempty"` // since QEMU 1.7
19+
Data json.RawMessage `json:"data,omitempty"` // since QEMU 1.7
20+
}
21+
22+
func (sp *InfoFormatSpecific) Qcow2() *InfoFormatSpecificDataQcow2 {
23+
if sp.Type != "qcow2" {
24+
return nil
25+
}
26+
var x InfoFormatSpecificDataQcow2
27+
if err := json.Unmarshal(sp.Data, &x); err != nil {
28+
panic(err)
29+
}
30+
return &x
31+
}
32+
33+
func (sp *InfoFormatSpecific) Vmdk() *InfoFormatSpecificDataVmdk {
34+
if sp.Type != "vmdk" {
35+
return nil
36+
}
37+
var x InfoFormatSpecificDataVmdk
38+
if err := json.Unmarshal(sp.Data, &x); err != nil {
39+
panic(err)
40+
}
41+
return &x
42+
}
43+
44+
type InfoFormatSpecificDataQcow2 struct {
45+
Compat string `json:"compat,omitempty"` // since QEMU 1.7
46+
LazyRefcounts bool `json:"lazy-refcounts,omitempty"` // since QEMU 1.7
47+
Corrupt bool `json:"corrupt,omitempty"` // since QEMU 2.2
48+
RefcountBits int `json:"refcount-bits,omitempty"` // since QEMU 2.3
49+
CompressionType string `json:"compression-type,omitempty"` // since QEMU 5.1
50+
ExtendedL2 bool `json:"extended-l2,omitempty"` // since QEMU 5.2
51+
}
52+
53+
type InfoFormatSpecificDataVmdk struct {
54+
CreateType string `json:"create-type,omitempty"` // since QEMU 1.7
55+
CID int `json:"cid,omitempty"` // since QEMU 1.7
56+
ParentCID int `json:"parent-cid,omitempty"` // since QEMU 1.7
57+
Extents []InfoFormatSpecificDataVmdkExtent `json:"extents,omitempty"` // since QEMU 1.7
58+
}
59+
60+
type InfoFormatSpecificDataVmdkExtent struct {
61+
Filename string `json:"filename,omitempty"` // since QEMU 1.7
62+
Format string `json:"format,omitempty"` // since QEMU 1.7
63+
VSize int64 `json:"virtual-size,omitempty"` // since QEMU 1.7
64+
ClusterSize int `json:"cluster-size,omitempty"` // since QEMU 1.7
65+
}
66+
1267
// Info corresponds to the output of `qemu-img info --output=json FILE`
1368
type Info struct {
14-
Format string `json:"format,omitempty"` // since QEMU 1.3
15-
VSize int64 `json:"virtual-size,omitempty"`
69+
Filename string `json:"filename,omitempty"` // since QEMU 1.3
70+
Format string `json:"format,omitempty"` // since QEMU 1.3
71+
VSize int64 `json:"virtual-size,omitempty"` // since QEMU 1.3
72+
ActualSize int64 `json:"actual-size,omitempty"` // since QEMU 1.3
73+
DirtyFlag bool `json:"dirty-flag,omitempty"` // since QEMU 5.2
74+
ClusterSize int `json:"cluster-size,omitempty"` // since QEMU 1.3
75+
BackingFilename string `json:"backing-filename,omitempty"` // since QEMU 1.3
76+
FullBackingFilename string `json:"full-backing-filename,omitempty"` // since QEMU 1.3
77+
BackingFilenameFormat string `json:"backing-filename-format,omitempty"` // since QEMU 1.3
78+
FormatSpecific *InfoFormatSpecific `json:"format-specific,omitempty"` // since QEMU 1.7
79+
Children []InfoChild `json:"children,omitempty"` // since QEMU 8.0
1680
}
1781

1882
func ConvertToRaw(source string, dest string) error {
@@ -27,6 +91,14 @@ func ConvertToRaw(source string, dest string) error {
2791
return nil
2892
}
2993

94+
func ParseInfo(b []byte) (*Info, error) {
95+
var imgInfo Info
96+
if err := json.Unmarshal(b, &imgInfo); err != nil {
97+
return nil, err
98+
}
99+
return &imgInfo, nil
100+
}
101+
30102
func GetInfo(f string) (*Info, error) {
31103
var stdout, stderr bytes.Buffer
32104
cmd := exec.Command("qemu-img", "info", "--output=json", "--force-share", f)
@@ -36,26 +108,45 @@ func GetInfo(f string) (*Info, error) {
36108
return nil, fmt.Errorf("failed to run %v: stdout=%q, stderr=%q: %w",
37109
cmd.Args, stdout.String(), stderr.String(), err)
38110
}
39-
var imgInfo Info
40-
if err := json.Unmarshal(stdout.Bytes(), &imgInfo); err != nil {
41-
return nil, err
42-
}
43-
return &imgInfo, nil
111+
return ParseInfo(stdout.Bytes())
44112
}
45113

46-
func DetectFormat(f string) (string, error) {
47-
switch ext := strings.ToLower(filepath.Ext(f)); ext {
48-
case ".qcow2":
49-
return "qcow2", nil
50-
case ".raw":
51-
return "raw", nil
114+
func AcceptableAsBasedisk(info *Info) error {
115+
switch info.Format {
116+
case "qcow2", "raw":
117+
// NOP
118+
default:
119+
logrus.WithField("filename", info.Filename).
120+
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)
121+
}
122+
if info.BackingFilename != "" {
123+
return fmt.Errorf("base disk (%q) must not have a backing file (%q)", info.Filename, info.BackingFilename)
52124
}
53-
imgInfo, err := GetInfo(f)
54-
if err != nil {
55-
return "", err
125+
if info.FullBackingFilename != "" {
126+
return fmt.Errorf("base disk (%q) must not have a backing file (%q)", info.Filename, info.FullBackingFilename)
56127
}
57-
if imgInfo.Format == "" {
58-
return "", fmt.Errorf("failed to detect format of %q", f)
128+
if info.FormatSpecific != nil {
129+
if vmdk := info.FormatSpecific.Vmdk(); vmdk != nil {
130+
for _, e := range vmdk.Extents {
131+
if e.Filename != info.Filename {
132+
return fmt.Errorf("base disk (%q) must not have an extent file (%q)", info.Filename, e.Filename)
133+
}
134+
}
135+
}
59136
}
60-
return imgInfo.Format, nil
137+
// info.Children is set since QEMU 8.0
138+
switch len(info.Children) {
139+
case 0:
140+
// NOP
141+
case 1:
142+
if info.Filename != info.Children[0].Info.Filename {
143+
return fmt.Errorf("base disk (%q) child must not have a different filename (%q)", info.Filename, info.Children[0].Info.Filename)
144+
}
145+
if len(info.Children[0].Info.Children) > 0 {
146+
return fmt.Errorf("base disk (%q) child must not have children of its own", info.Filename)
147+
}
148+
default:
149+
return fmt.Errorf("base disk (%q) must not have multiple children: %+v", info.Filename, info.Children)
150+
}
151+
return nil
61152
}

pkg/qemu/imgutil/imgutil_test.go

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
package imgutil
2+
3+
import (
4+
"testing"
5+
6+
"gotest.tools/v3/assert"
7+
)
8+
9+
func TestParseInfo(t *testing.T) {
10+
t.Run("qcow2", func(t *testing.T) {
11+
// qemu-img create -f qcow2 foo.qcow2 4G
12+
// (QEMU 8.0)
13+
const s = `{
14+
"children": [
15+
{
16+
"name": "file",
17+
"info": {
18+
"children": [
19+
],
20+
"virtual-size": 197120,
21+
"filename": "foo.qcow2",
22+
"format": "file",
23+
"actual-size": 200704,
24+
"format-specific": {
25+
"type": "file",
26+
"data": {
27+
}
28+
},
29+
"dirty-flag": false
30+
}
31+
}
32+
],
33+
"virtual-size": 4294967296,
34+
"filename": "foo.qcow2",
35+
"cluster-size": 65536,
36+
"format": "qcow2",
37+
"actual-size": 200704,
38+
"format-specific": {
39+
"type": "qcow2",
40+
"data": {
41+
"compat": "1.1",
42+
"compression-type": "zlib",
43+
"lazy-refcounts": false,
44+
"refcount-bits": 16,
45+
"corrupt": false,
46+
"extended-l2": false
47+
}
48+
},
49+
"dirty-flag": false
50+
}`
51+
52+
info, err := ParseInfo([]byte(s))
53+
assert.NilError(t, err)
54+
assert.Equal(t, 1, len(info.Children))
55+
assert.Check(t, info.FormatSpecific != nil)
56+
qcow2 := info.FormatSpecific.Qcow2()
57+
assert.Check(t, qcow2 != nil)
58+
assert.Equal(t, qcow2.Compat, "1.1")
59+
60+
t.Run("diff", func(t *testing.T) {
61+
// qemu-img create -f qcow2 -F qcow2 -b foo.qcow2 bar.qcow2
62+
// (QEMU 8.0)
63+
const s = `{
64+
"children": [
65+
{
66+
"name": "file",
67+
"info": {
68+
"children": [
69+
],
70+
"virtual-size": 197120,
71+
"filename": "bar.qcow2",
72+
"format": "file",
73+
"actual-size": 200704,
74+
"format-specific": {
75+
"type": "file",
76+
"data": {
77+
}
78+
},
79+
"dirty-flag": false
80+
}
81+
}
82+
],
83+
"backing-filename-format": "qcow2",
84+
"virtual-size": 4294967296,
85+
"filename": "bar.qcow2",
86+
"cluster-size": 65536,
87+
"format": "qcow2",
88+
"actual-size": 200704,
89+
"format-specific": {
90+
"type": "qcow2",
91+
"data": {
92+
"compat": "1.1",
93+
"compression-type": "zlib",
94+
"lazy-refcounts": false,
95+
"refcount-bits": 16,
96+
"corrupt": false,
97+
"extended-l2": false
98+
}
99+
},
100+
"full-backing-filename": "foo.qcow2",
101+
"backing-filename": "foo.qcow2",
102+
"dirty-flag": false
103+
}`
104+
info, err := ParseInfo([]byte(s))
105+
assert.NilError(t, err)
106+
assert.Equal(t, 1, len(info.Children))
107+
assert.Equal(t, "foo.qcow2", info.BackingFilename)
108+
assert.Equal(t, "bar.qcow2", info.Filename)
109+
assert.Check(t, info.FormatSpecific != nil)
110+
qcow2 := info.FormatSpecific.Qcow2()
111+
assert.Check(t, qcow2 != nil)
112+
assert.Equal(t, qcow2.Compat, "1.1")
113+
})
114+
})
115+
t.Run("vmdk", func(t *testing.T) {
116+
t.Run("twoGbMaxExtentSparse", func(t *testing.T) {
117+
// qemu-img create -f vmdk foo.vmdk 4G -o subformat=twoGbMaxExtentSparse
118+
// (QEMU 8.0)
119+
const s = `{
120+
"children": [
121+
{
122+
"name": "extents.1",
123+
"info": {
124+
"children": [
125+
],
126+
"virtual-size": 327680,
127+
"filename": "foo-s002.vmdk",
128+
"format": "file",
129+
"actual-size": 327680,
130+
"format-specific": {
131+
"type": "file",
132+
"data": {
133+
}
134+
},
135+
"dirty-flag": false
136+
}
137+
},
138+
{
139+
"name": "extents.0",
140+
"info": {
141+
"children": [
142+
],
143+
"virtual-size": 327680,
144+
"filename": "foo-s001.vmdk",
145+
"format": "file",
146+
"actual-size": 327680,
147+
"format-specific": {
148+
"type": "file",
149+
"data": {
150+
}
151+
},
152+
"dirty-flag": false
153+
}
154+
},
155+
{
156+
"name": "file",
157+
"info": {
158+
"children": [
159+
],
160+
"virtual-size": 512,
161+
"filename": "foo.vmdk",
162+
"format": "file",
163+
"actual-size": 4096,
164+
"format-specific": {
165+
"type": "file",
166+
"data": {
167+
}
168+
},
169+
"dirty-flag": false
170+
}
171+
}
172+
],
173+
"virtual-size": 4294967296,
174+
"filename": "foo.vmdk",
175+
"cluster-size": 65536,
176+
"format": "vmdk",
177+
"actual-size": 659456,
178+
"format-specific": {
179+
"type": "vmdk",
180+
"data": {
181+
"cid": 918420663,
182+
"parent-cid": 4294967295,
183+
"create-type": "twoGbMaxExtentSparse",
184+
"extents": [
185+
{
186+
"virtual-size": 2147483648,
187+
"filename": "foo-s001.vmdk",
188+
"cluster-size": 65536,
189+
"format": "SPARSE"
190+
},
191+
{
192+
"virtual-size": 2147483648,
193+
"filename": "foo-s002.vmdk",
194+
"cluster-size": 65536,
195+
"format": "SPARSE"
196+
}
197+
]
198+
}
199+
},
200+
"dirty-flag": false
201+
}`
202+
info, err := ParseInfo([]byte(s))
203+
assert.NilError(t, err)
204+
assert.Equal(t, 3, len(info.Children))
205+
assert.Equal(t, "foo.vmdk", info.Filename)
206+
assert.Check(t, info.FormatSpecific != nil)
207+
vmdk := info.FormatSpecific.Vmdk()
208+
assert.Check(t, vmdk != nil)
209+
assert.Equal(t, vmdk.CreateType, "twoGbMaxExtentSparse")
210+
})
211+
})
212+
}

0 commit comments

Comments
 (0)