Skip to content

Commit c034393

Browse files
authored
cmd fuzz implementation (#34)
1 parent 643e5d1 commit c034393

File tree

7 files changed

+260
-11
lines changed

7 files changed

+260
-11
lines changed

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
NOW=`date '+%Y.%m.%d %H:%M:%S'`
2-
OS=`uname -i -v`
2+
OS=`uname`
33
AFTER_COMMIT=`git rev-parse HEAD`
44
VERSION=0.3.0
55

cmd/phpsmith/cmd_fuzz.go

+154-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,164 @@
11
package main
22

3-
import "flag"
3+
import (
4+
"bytes"
5+
"context"
6+
"flag"
7+
"log"
8+
"math/rand"
9+
"os"
10+
"os/signal"
11+
"runtime"
12+
"strconv"
13+
"syscall"
14+
"time"
15+
16+
"github.com/google/go-cmp/cmp"
17+
"golang.org/x/sync/errgroup"
18+
19+
"github.com/quasilyte/phpsmith/cmd/phpsmith/interpretator"
20+
)
21+
22+
type executor func(ctx context.Context, filename string, seed int64) ([]byte, error)
23+
24+
var executors = []executor{
25+
interpretator.RunPHP,
26+
interpretator.RunKPHP,
27+
}
428

529
func cmdFuzz(args []string) error {
630
fs := flag.NewFlagSet("phpsmith fuzz", flag.ExitOnError)
31+
flagConcurrency := fs.Int("flagConcurrency", 1,
32+
"Number of concurrent runners. Defaults to the half number of available CPU cores.")
33+
flagSeed := fs.Int64("seed", 0,
34+
`a seed to be used during the code generation, 0 means "randomized seed"`)
35+
flagOutputDir := fs.String("o", "phpsmith_out",
36+
`output dir`)
37+
738
_ = fs.Parse(args)
839

9-
// TODO
40+
concurrency := *flagConcurrency
41+
dir := *flagOutputDir
42+
seed := *flagSeed
43+
44+
if concurrency == 1 && runtime.NumCPU()/2 > 1 {
45+
concurrency = runtime.NumCPU() / 2
46+
}
47+
48+
if seed == 0 {
49+
seed = time.Now().Unix()
50+
}
51+
52+
interrupt := make(chan os.Signal, 1)
53+
signalNotify(interrupt)
54+
55+
eg, ctx := errgroup.WithContext(context.Background())
56+
ctx, cancel := context.WithCancel(ctx)
57+
58+
go func() {
59+
<-interrupt
60+
cancel()
61+
}()
62+
63+
dirCh := make(chan string, concurrency)
64+
for i := 0; i < concurrency; i++ {
65+
eg.Go(func() error {
66+
return runner(ctx, dirCh, seed)
67+
})
68+
}
69+
70+
randomizer := rand.New(rand.NewSource(time.Now().Unix()))
71+
out:
72+
for {
73+
newDir := dir + strconv.Itoa(randomizer.Int())
74+
if err := generate(newDir, seed); err != nil {
75+
log.Println("on generate: ", err)
76+
continue
77+
}
78+
79+
select {
80+
case dirCh <- newDir:
81+
case <-ctx.Done():
82+
break out
83+
}
84+
}
85+
86+
if err := eg.Wait(); err != nil {
87+
log.Println("on errorGroup Execution: ", err)
88+
}
1089

1190
return nil
1291
}
92+
93+
func runner(ctx context.Context, dirs <-chan string, seed int64) error {
94+
for {
95+
select {
96+
case <-ctx.Done():
97+
return nil
98+
case dir := <-dirs:
99+
if err := fuzzingProcess(ctx, dir, seed); err != nil {
100+
log.Println("on fuzzingProcess:", err)
101+
}
102+
log.Println("dir processed:", dir)
103+
}
104+
}
105+
}
106+
107+
type ExecutorOutput struct {
108+
Output string
109+
Error string
110+
}
111+
112+
func fuzzingProcess(ctx context.Context, dir string, seed int64) error {
113+
var results = make(map[int]ExecutorOutput, len(executors))
114+
for i, ex := range executors {
115+
var errMsg string
116+
r, err := ex(ctx, dir, seed)
117+
118+
if err != nil {
119+
errMsg = err.Error()
120+
}
121+
122+
grepExceptions(r, seed)
123+
results[i] = ExecutorOutput{
124+
Output: string(r),
125+
Error: errMsg,
126+
}
127+
}
128+
129+
if diff := cmp.Diff(results[0].Output, results[1].Output); diff != "" {
130+
l, err := os.OpenFile("./"+dir+"/log", os.O_RDWR|os.O_CREATE, 0700)
131+
if err != nil {
132+
log.Println("-----------------------------")
133+
log.Printf("diff: %s\t, seed: %d\t\n", diff, seed)
134+
log.Printf("out: %s\terr: %s\t\n", results[0].Output, results[0].Error)
135+
log.Printf("out: %s\terr: %s\t\n", results[1].Output, results[1].Error)
136+
} else {
137+
defer l.Close()
138+
139+
logger := log.New(l, "", 0)
140+
logger.Printf("diff: %s\t, seed: %d\t\n", diff, seed)
141+
logger.Printf("out: %s\terr: %s\t\n", results[0].Output, results[0].Error)
142+
logger.Printf("out: %s\terr: %s\t\n", results[1].Output, results[1].Error)
143+
}
144+
}
145+
146+
return nil
147+
}
148+
149+
func signalNotify(interrupt chan<- os.Signal) {
150+
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
151+
}
152+
153+
var exceptionPatterns = [][]byte{
154+
[]byte("uncaught exception"),
155+
[]byte("fatal error"),
156+
}
157+
158+
func grepExceptions(s []byte, seed int64) {
159+
for _, pattern := range exceptionPatterns {
160+
if bytes.Contains(bytes.ToLower(s), pattern) {
161+
log.Println("found exception pattern: on seed:", seed)
162+
}
163+
}
164+
}

cmd/phpsmith/cmd_generate.go

+14-7
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,24 @@ import (
1515

1616
func cmdGenerate(args []string) error {
1717
fs := flag.NewFlagSet("phpsmith generate", flag.ExitOnError)
18-
flagSeed := fs.Uint64("seed", 0,
18+
flagSeed := fs.Int64("seed", 0,
1919
`a seed to be used during the code generation, 0 means "randomized seed"`)
2020
flagOutputDir := fs.String("o", "phpsmith_out",
2121
`output dir`)
2222
_ = fs.Parse(args)
2323

24-
randomSeed := int64(*flagSeed)
25-
if randomSeed == 0 {
26-
randomSeed = time.Now().Unix()
24+
var seed int64
25+
if *flagSeed == 0 {
26+
seed = time.Now().Unix()
2727
}
28+
29+
return generate(*flagOutputDir, seed)
30+
}
31+
32+
func generate(dir string, randomSeed int64) error {
2833
random := rand.New(rand.NewSource(randomSeed))
2934

30-
if err := os.MkdirAll(*flagOutputDir, 0o700); err != nil {
35+
if err := os.MkdirAll(dir, 0o700); err != nil && !os.IsExist(err) {
3136
return err
3237
}
3338

@@ -36,14 +41,16 @@ func cmdGenerate(args []string) error {
3641
printerConfig := &irprint.Config{
3742
Rand: random,
3843
}
44+
3945
for _, f := range program.RuntimeFiles {
40-
fullname := filepath.Join(*flagOutputDir, f.Name)
46+
fullname := filepath.Join(dir, f.Name)
4147
if err := os.WriteFile(fullname, f.Contents, 0o664); err != nil {
4248
return fmt.Errorf("create %s file: %w", fullname, err)
4349
}
4450
}
51+
4552
for _, f := range program.Files {
46-
fullname := filepath.Join(*flagOutputDir, f.Name)
53+
fullname := filepath.Join(dir, f.Name)
4754
fileContents := makeFileContents(f, printerConfig)
4855
if err := os.WriteFile(fullname, fileContents, 0o664); err != nil {
4956
return fmt.Errorf("create %s file: %w", fullname, err)

cmd/phpsmith/interpretator/kphp.go

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package interpretator
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"log"
8+
"os/exec"
9+
"sync"
10+
)
11+
12+
var mu sync.Mutex
13+
14+
func RunKPHP(ctx context.Context, dir string, seed int64) ([]byte, error) {
15+
var (
16+
outBuffer bytes.Buffer
17+
errBuffer bytes.Buffer
18+
)
19+
20+
binaryName := dir + "/" + dir
21+
22+
if err := func() error {
23+
mu.Lock()
24+
defer mu.Unlock()
25+
26+
compileCmd := exec.CommandContext(ctx, "kphp", "--mode", "cli", "-o", binaryName, dir+"/main.php")
27+
compileCmd.Stdout, compileCmd.Stderr = &outBuffer, &errBuffer
28+
29+
if err := compileCmd.Run(); err != nil {
30+
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() > 1 {
31+
log.Printf("non one or zero exit code found: %d, seed: %d \n", exitErr.ExitCode(), seed)
32+
}
33+
return err
34+
}
35+
return nil
36+
}(); err != nil {
37+
return nil, fmt.Errorf("on compile kphp: stdOut: %s, stdErr: %s", outBuffer.String(), errBuffer.String())
38+
}
39+
40+
outBuffer.Reset()
41+
errBuffer.Reset()
42+
43+
runCmd := exec.CommandContext(ctx, binaryName)
44+
runCmd.Stdout, runCmd.Stderr = &outBuffer, &errBuffer
45+
46+
if err := runCmd.Run(); err != nil {
47+
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() > 1 {
48+
log.Printf("non one or zero exit code found: %d, seed: %d \n", exitErr.ExitCode(), seed)
49+
}
50+
return nil, fmt.Errorf("on run kphp binary: stdOut: %s, stdErr: %s", outBuffer.String(), errBuffer.String())
51+
}
52+
53+
return outBuffer.Bytes(), nil
54+
}

cmd/phpsmith/interpretator/php.go

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package interpretator
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"log"
8+
"os/exec"
9+
)
10+
11+
func RunPHP(ctx context.Context, dir string, seed int64) ([]byte, error) {
12+
var (
13+
outBuffer bytes.Buffer
14+
errBuffer bytes.Buffer
15+
)
16+
phpCmd := exec.CommandContext(ctx, "php", "-f", dir+"/main.php")
17+
phpCmd.Stdout, phpCmd.Stderr = &outBuffer, &errBuffer
18+
19+
if err := phpCmd.Run(); err != nil {
20+
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() > 1 {
21+
log.Printf("non one or zero exit code found: %d, seed: %d \n", exitErr.ExitCode(), seed)
22+
}
23+
return nil, fmt.Errorf("on run php: stdOut: %s, stdErr: %s", outBuffer.String(), errBuffer.String())
24+
}
25+
26+
return outBuffer.Bytes(), nil
27+
}

go.mod

+6-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,9 @@ module github.com/quasilyte/phpsmith
22

33
go 1.17
44

5-
require github.com/cespare/subcmd v1.1.0
5+
require (
6+
github.com/cespare/subcmd v1.1.0
7+
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
8+
)
9+
10+
require github.com/google/go-cmp v0.5.8 // indirect

go.sum

+4
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
github.com/cespare/subcmd v1.1.0 h1:r60BAqAKOGcBjxHmV9/WYvq5Qbp3xW9ByB+fRjtty9U=
22
github.com/cespare/subcmd v1.1.0/go.mod h1:wnVjukiuhSlhZSgGHUilbkHykG7Oglb0sJXpUQ+MoUw=
3+
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
4+
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
5+
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
6+
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

0 commit comments

Comments
 (0)