You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

161 lines
5.1 KiB

  1. // Copyright (c) 2016 Marty Schoch
  2. // Licensed under the Apache License, Version 2.0 (the "License");
  3. // you may not use this file except in compliance with the
  4. // License. You may obtain a copy of the License at
  5. // http://www.apache.org/licenses/LICENSE-2.0
  6. // Unless required by applicable law or agreed to in writing,
  7. // software distributed under the License is distributed on an "AS
  8. // IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
  9. // express or implied. See the License for the specific language
  10. // governing permissions and limitations under the License.
  11. package smat
  12. import (
  13. "bufio"
  14. "bytes"
  15. "fmt"
  16. "io"
  17. "io/ioutil"
  18. "log"
  19. "math/rand"
  20. )
  21. // Logger is a configurable logger used by this package
  22. // by default output is discarded
  23. var Logger = log.New(ioutil.Discard, "smat ", log.LstdFlags)
  24. // Context is a container for any user state
  25. type Context interface{}
  26. // State is a function which describes which action to perform in the event
  27. // that a particular byte is seen
  28. type State func(next byte) ActionID
  29. // PercentAction describes the frequency with which an action should occur
  30. // for example: Action{Percent:10, Action:DonateMoney} means that 10% of
  31. // the time you should donate money.
  32. type PercentAction struct {
  33. Percent int
  34. Action ActionID
  35. }
  36. // Action is any function which returns the next state to transition to
  37. // it can optionally mutate the provided context object
  38. // if any error occurs, it may return an error which will abort execution
  39. type Action func(Context) (State, error)
  40. // ActionID is a unique identifier for an action
  41. type ActionID int
  42. // NopAction does nothing and simply continues to the next input
  43. var NopAction ActionID = -1
  44. // ActionMap is a mapping form ActionID to Action
  45. type ActionMap map[ActionID]Action
  46. func (a ActionMap) findSetupTeardown(setup, teardown ActionID) (Action, Action, error) {
  47. setupFunc, ok := a[setup]
  48. if !ok {
  49. return nil, nil, ErrSetupMissing
  50. }
  51. teardownFunc, ok := a[teardown]
  52. if !ok {
  53. return nil, nil, ErrTeardownMissing
  54. }
  55. return setupFunc, teardownFunc, nil
  56. }
  57. // Fuzz runs the fuzzing state machine with the provided context
  58. // first, the setup action is executed unconditionally
  59. // the start state is determined by this action
  60. // actionMap is a lookup table for all actions
  61. // the data byte slice determines all future state transitions
  62. // finally, the teardown action is executed unconditionally for cleanup
  63. func Fuzz(ctx Context, setup, teardown ActionID, actionMap ActionMap, data []byte) int {
  64. reader := bytes.NewReader(data)
  65. err := runReader(ctx, setup, teardown, actionMap, reader, nil)
  66. if err != nil {
  67. panic(err)
  68. }
  69. return 1
  70. }
  71. // Longevity runs the state machine with the provided context
  72. // first, the setup action is executed unconditionally
  73. // the start state is determined by this action
  74. // actionMap is a lookup table for all actions
  75. // random bytes are generated to determine all future state transitions
  76. // finally, the teardown action is executed unconditionally for cleanup
  77. func Longevity(ctx Context, setup, teardown ActionID, actionMap ActionMap, seed int64, closeChan chan struct{}) error {
  78. source := rand.NewSource(seed)
  79. return runReader(ctx, setup, teardown, actionMap, rand.New(source), closeChan)
  80. }
  81. var (
  82. // ErrSetupMissing is returned when the setup action cannot be found
  83. ErrSetupMissing = fmt.Errorf("setup action missing")
  84. // ErrTeardownMissing is returned when the teardown action cannot be found
  85. ErrTeardownMissing = fmt.Errorf("teardown action missing")
  86. // ErrClosed is returned when the closeChan was closed to cancel the op
  87. ErrClosed = fmt.Errorf("closed")
  88. // ErrActionNotPossible is returned when an action is encountered in a
  89. // FuzzCase that is not possible in the current state
  90. ErrActionNotPossible = fmt.Errorf("action not possible in state")
  91. )
  92. func runReader(ctx Context, setup, teardown ActionID, actionMap ActionMap, r io.Reader, closeChan chan struct{}) error {
  93. setupFunc, teardownFunc, err := actionMap.findSetupTeardown(setup, teardown)
  94. if err != nil {
  95. return err
  96. }
  97. Logger.Printf("invoking setup action")
  98. state, err := setupFunc(ctx)
  99. if err != nil {
  100. return err
  101. }
  102. defer func() {
  103. Logger.Printf("invoking teardown action")
  104. _, _ = teardownFunc(ctx)
  105. }()
  106. reader := bufio.NewReader(r)
  107. for next, err := reader.ReadByte(); err == nil; next, err = reader.ReadByte() {
  108. select {
  109. case <-closeChan:
  110. return ErrClosed
  111. default:
  112. actionID := state(next)
  113. action, ok := actionMap[actionID]
  114. if !ok {
  115. Logger.Printf("no such action defined, continuing")
  116. continue
  117. }
  118. Logger.Printf("invoking action - %d", actionID)
  119. state, err = action(ctx)
  120. if err != nil {
  121. Logger.Printf("it was action %d that returned err %v", actionID, err)
  122. return err
  123. }
  124. }
  125. }
  126. return err
  127. }
  128. // PercentExecute interprets the next byte as a random value and normalizes it
  129. // to values 0-99, it then looks to see which action should be execued based
  130. // on the action distributions
  131. func PercentExecute(next byte, pas ...PercentAction) ActionID {
  132. percent := int(99 * int(next) / 255)
  133. sofar := 0
  134. for _, pa := range pas {
  135. sofar = sofar + pa.Percent
  136. if percent < sofar {
  137. return pa.Action
  138. }
  139. }
  140. return NopAction
  141. }