Skip to content

Commit 3a0c205

Browse files
committed
tools/syz-imagegen: rewrite combination generation
Introduce a new Filesystem parameter - the maximum number of resulting seeds. If the total number of flag combinations exceeds this number, switch to generating a covering array (that is, make sure that all flag value pairs are covered, or at least as many of them as possible).
1 parent 7d5925b commit 3a0c205

File tree

3 files changed

+252
-19
lines changed

3 files changed

+252
-19
lines changed

tools/syz-imagegen/combinations.go

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
// Copyright 2025 syzkaller project authors. All rights reserved.
2+
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
3+
4+
package main
5+
6+
import "sort"
7+
8+
// CoveringArray returns an array of representative parameter value combinations.
9+
// The method considers parameters from the first to the last and first tries to
10+
// generate all possible combinations of their values.
11+
// If N != 0, the method eventually switches to ensuring that all value pairs
12+
// are represented, once all pairs are covered - all triples (aka Covering Array).
13+
func CoveringArray(params [][]string, n int) [][]string {
14+
var ret [][]int
15+
for paramID, param := range params {
16+
if len(ret) == 0 {
17+
ret = append(ret, []int{})
18+
}
19+
// If we can explore all combinations, do it.
20+
if len(ret)*len(param) <= n || n == 0 {
21+
var newRet [][]int
22+
for value := range param {
23+
for _, row := range ret {
24+
newRet = append(newRet, extendRow(row, value))
25+
}
26+
}
27+
ret = newRet
28+
continue
29+
}
30+
cover := &pairCoverage{cover: map[pairCombo]struct{}{}}
31+
32+
// First, select a value for each row.
33+
var newRet [][]int
34+
for _, row := range ret {
35+
bestValue, bestCount := 0, coverDelta{}
36+
for valueID := range param {
37+
newCount := cover.wouldCover(row, paramID, valueID)
38+
if newCount.betterThan(bestCount) {
39+
bestValue = valueID
40+
bestCount = newCount
41+
}
42+
}
43+
newRet = append(newRet, extendRow(row, bestValue))
44+
cover.record(row, paramID, bestValue)
45+
}
46+
47+
// Now that all previous combinations are preserved, we can (as long as
48+
// we don't exceed N) duplicate some of the rows to cover more.
49+
for len(newRet) < n {
50+
var bestRow []int
51+
bestValue, bestCount := 0, coverDelta{}
52+
for _, row := range ret {
53+
for valueID := range param {
54+
newCount := cover.wouldCover(row, paramID, valueID)
55+
if newCount.betterThan(bestCount) {
56+
bestRow = row
57+
bestValue = valueID
58+
bestCount = newCount
59+
}
60+
}
61+
}
62+
if !bestCount.betterThan(coverDelta{}) {
63+
break
64+
}
65+
newRet = append(newRet, extendRow(bestRow, bestValue))
66+
cover.record(bestRow, paramID, bestValue)
67+
}
68+
ret = newRet
69+
}
70+
sort.Slice(ret, func(i, j int) bool {
71+
rowA, rowB := ret[i], ret[j]
72+
for k := 0; k < len(rowA); k++ {
73+
if rowA[k] != rowB[k] {
74+
return rowA[k] < rowB[k]
75+
}
76+
}
77+
return false
78+
})
79+
var retStrings [][]string
80+
for _, row := range ret {
81+
var stringRow []string
82+
for paramID, valueID := range row {
83+
stringRow = append(stringRow, params[paramID][valueID])
84+
}
85+
retStrings = append(retStrings, stringRow)
86+
}
87+
return retStrings
88+
}
89+
90+
type pairCoverage struct {
91+
cover map[pairCombo]struct{}
92+
}
93+
94+
type coverDelta struct {
95+
pairs int
96+
triples int
97+
}
98+
99+
func (c coverDelta) betterThan(other coverDelta) bool {
100+
if c.pairs != other.pairs {
101+
return c.pairs > other.pairs
102+
}
103+
return c.triples > other.triples
104+
}
105+
106+
// By how much the coverage would increase if we append newVal to the row.
107+
// The first integer is the number of newly covered pairs of values,
108+
// the second integer is the number of newly covered triples of values.
109+
func (pc *pairCoverage) wouldCover(row []int, newID, newVal int) coverDelta {
110+
var pairs, triples int
111+
for _, item := range rowToPairCombos(row, false, newID, newVal) {
112+
if _, ok := pc.cover[item]; !ok {
113+
pairs++
114+
}
115+
}
116+
for _, item := range rowToPairCombos(row, true, newID, newVal) {
117+
if _, ok := pc.cover[item]; !ok {
118+
triples++
119+
}
120+
}
121+
return coverDelta{pairs, triples}
122+
}
123+
124+
func (pc *pairCoverage) record(row []int, newID, newVal int) {
125+
for _, item := range append(
126+
rowToPairCombos(row, false, newID, newVal),
127+
rowToPairCombos(row, true, newID, newVal)...) {
128+
pc.cover[item] = struct{}{}
129+
}
130+
}
131+
132+
type pair struct {
133+
pos int
134+
value int
135+
}
136+
137+
type pairCombo struct {
138+
first pair
139+
second pair
140+
third pair
141+
}
142+
143+
func rowToPairCombos(row []int, triples bool, newID, newVal int) []pairCombo {
144+
var ret []pairCombo
145+
// All things being equal, we want to also favor more different values.
146+
ret = append(ret, pairCombo{third: pair{newID + 1, newVal}})
147+
for i := 0; i+1 < len(row); i++ {
148+
if !triples {
149+
ret = append(ret, pairCombo{
150+
first: pair{i + 1, row[i]},
151+
third: pair{newID + 1, newVal},
152+
})
153+
continue
154+
}
155+
for j := i + 1; j < len(row); j++ {
156+
ret = append(ret, pairCombo{
157+
first: pair{i + 1, row[i]},
158+
second: pair{j + 1, row[j]},
159+
third: pair{newID + 1, newVal},
160+
})
161+
}
162+
}
163+
return ret
164+
}
165+
166+
func extendRow(row []int, newVal int) []int {
167+
return append(append([]int{}, row...), newVal)
168+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright 2025 syzkaller project authors. All rights reserved.
2+
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
3+
4+
package main
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestFullCombinations(t *testing.T) {
13+
t.Run("empty", func(t *testing.T) {
14+
assert.Len(t, CoveringArray([][]string{}, 0), 0)
15+
})
16+
t.Run("single", func(t *testing.T) {
17+
assert.Equal(t, [][]string{
18+
{"A", "B", "C"},
19+
}, CoveringArray([][]string{
20+
{"A"},
21+
{"B"},
22+
{"C"},
23+
}, 0))
24+
})
25+
t.Run("binary", func(t *testing.T) {
26+
assert.Equal(t, [][]string{
27+
{"A", "B", "C"},
28+
{"A", "B", "c"},
29+
{"A", "b", "C"},
30+
{"A", "b", "c"},
31+
{"a", "B", "C"},
32+
{"a", "B", "c"},
33+
{"a", "b", "C"},
34+
{"a", "b", "c"},
35+
}, CoveringArray([][]string{
36+
{"A", "a"},
37+
{"B", "b"},
38+
{"C", "c"},
39+
}, 0))
40+
})
41+
}
42+
43+
func TestPairCombinations(t *testing.T) {
44+
// Theoretically, there may be multiple correct answers.
45+
// For now, let's keep the current algorithm's output so that if the code behavior changes unexpectedly,
46+
// we'd notice.
47+
assert.Equal(t, [][]string{
48+
{"A", "B", "C"},
49+
{"A", "b", "c"},
50+
{"a", "B", "c"},
51+
{"a", "b", "C"},
52+
}, CoveringArray([][]string{
53+
{"A", "a"},
54+
{"B", "b"},
55+
{"C", "c"},
56+
}, 4))
57+
}

tools/syz-imagegen/imagegen.go

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"os/exec"
2626
"path/filepath"
2727
"runtime"
28+
"slices"
2829
"strings"
2930
"syscall"
3031
"time"
@@ -53,6 +54,8 @@ type FileSystem struct {
5354
MkfsFlags []string `json:"mkfs_flags"`
5455
// Generate images for all possible permutations of these flag combinations.
5556
MkfsFlagCombinations [][]string `json:"mkfs_flag_combinations"`
57+
// Limit the resulting number of seeds (in case there are too many possible combinations).
58+
MaxSeeds int `json:"max_seeds"`
5659
// Custom mkfs invocation, if nil then mkfs.name is invoked in a standard way.
5760
Mkfs func(img *Image) error
5861
}
@@ -758,10 +761,13 @@ func generateImages(target *prog.Target, flagFS string, list bool) ([]*Image, er
758761
if flagFS != "" && !strings.Contains(","+flagFS+",", ","+fs.suffix()+",") {
759762
continue
760763
}
761-
index := 0
762-
enumerateFlags(target, &images, &index, fs, fs.MkfsFlags, 0)
764+
newImages := enumerateFlags(target, fs)
765+
images = append(images, newImages...)
763766
if list {
764-
fmt.Printf("%v [%v images]\n", fs.Name, index)
767+
fmt.Printf("%v [%v images]\n", fs.Name, len(newImages))
768+
for i, image := range newImages {
769+
fmt.Printf("#%d: %q\n", i, image.flags)
770+
}
765771
continue
766772
}
767773
files, err := filepath.Glob(filepath.Join("sys", targets.Linux, "test", fs.filePrefix()+"_*"))
@@ -774,30 +780,32 @@ func generateImages(target *prog.Target, flagFS string, list bool) ([]*Image, er
774780
}
775781
}
776782
}
783+
for i, image := range images {
784+
image.index = i
785+
}
777786
return images, nil
778787
}
779788

780-
func enumerateFlags(target *prog.Target, images *[]*Image, index *int, fs FileSystem, flags []string, flagsIndex int) {
781-
if flagsIndex == len(fs.MkfsFlagCombinations) {
782-
*images = append(*images, &Image{
789+
func enumerateFlags(target *prog.Target, fs FileSystem) []*Image {
790+
var images []*Image
791+
for _, flags := range CoveringArray(fs.MkfsFlagCombinations, fs.MaxSeeds) {
792+
imageFlags := slices.Clone(fs.MkfsFlags)
793+
for _, rawFlag := range flags {
794+
for _, f := range strings.Split(rawFlag, " ") {
795+
if f == "" {
796+
continue
797+
}
798+
imageFlags = append(imageFlags, f)
799+
}
800+
}
801+
images = append(images, &Image{
783802
target: target,
784803
fs: fs,
785-
flags: append([]string{}, flags...),
786-
index: *index,
804+
flags: imageFlags,
787805
done: make(chan error, 1),
788806
})
789-
*index++
790-
return
791-
}
792-
for _, flag := range fs.MkfsFlagCombinations[flagsIndex] {
793-
flags1 := flags
794-
for _, f := range strings.Split(flag, " ") {
795-
if f != "" {
796-
flags1 = append(flags1, f)
797-
}
798-
}
799-
enumerateFlags(target, images, index, fs, flags1, flagsIndex+1)
800807
}
808+
return images
801809
}
802810

803811
func (img *Image) generate() error {

0 commit comments

Comments
 (0)