Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions internal/corazawaf/rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,12 +265,12 @@ func (r *Rule) doEvaluate(logger debuglog.Logger, phase types.RulePhase, tx *Tra
args := make([]string, 1)
var errs []error
var argsLen int
for i, arg := range values {
for _, arg := range values {
if r.MultiMatch {
args, errs = r.transformMultiMatchArg(arg)
argsLen = len(args)
} else {
args[0], errs = r.transformArg(arg, i, cache)
args[0], errs = r.transformArg(arg, cache)
argsLen = 1
}
if len(errs) > 0 {
Expand Down Expand Up @@ -424,7 +424,7 @@ func (r *Rule) transformMultiMatchArg(arg types.MatchData) ([]string, []error) {
return r.executeTransformationsMultimatch(arg.Value())
}

func (r *Rule) transformArg(arg types.MatchData, argIdx int, cache map[transformationKey]transformationValue) (string, []error) {
func (r *Rule) transformArg(arg types.MatchData, cache map[transformationKey]transformationValue) (string, []error) {
switch {
case len(r.transformations) == 0:
return arg.Value(), nil
Expand All @@ -434,20 +434,20 @@ func (r *Rule) transformArg(arg types.MatchData, argIdx int, cache map[transform
return arg, errs
default:
// NOTE: See comment on transformationKey struct to understand this hacky code
argKey := arg.Key()
argKeyPtr := unsafe.StringData(argKey)
argValue := arg.Value()
argValuePtr := unsafe.StringData(argValue)

// Search from longest prefix (full chain) backwards for a cache hit.
// Best case: full chain cached → single map lookup, done.
// Typical case: shared prefix cached → start computing from there.
startIdx := 0
value := arg.Value()
value := argValue
var errs []error

for i := len(r.transformationPrefixIDs) - 1; i >= 0; i-- {
key := transformationKey{
argKey: argKeyPtr,
argIndex: argIdx,
argValue: argValuePtr,
argValueLen: len(argValue),
argVariable: arg.Variable(),
transformationsID: r.transformationPrefixIDs[i],
}
Expand All @@ -474,8 +474,8 @@ func (r *Rule) transformArg(arg types.MatchData, argIdx int, cache map[transform
}

key := transformationKey{
argKey: argKeyPtr,
argIndex: argIdx,
argValue: argValuePtr,
argValueLen: len(argValue),
argVariable: arg.Variable(),
transformationsID: r.transformationPrefixIDs[i],
}
Expand Down
33 changes: 26 additions & 7 deletions internal/corazawaf/rule_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -623,7 +623,7 @@ func TestTransformArgSimple(t *testing.T) {
rule := NewRule()
_ = rule.AddTransformation("AppendA", transformationAppendA)
_ = rule.AddTransformation("AppendB", transformationAppendB)
arg, errs := rule.transformArg(md, 0, transformationCache)
arg, errs := rule.transformArg(md, transformationCache)
if errs != nil {
t.Fatalf("Unexpected errors executing transformations: %v", errs)
}
Expand All @@ -635,7 +635,7 @@ func TestTransformArgSimple(t *testing.T) {
t.Errorf("Expected 2 transformations in cache (one per step), got %d", len(transformationCache))
}
// Repeating the same transformation, expecting still two elements in the cache (cache hit)
arg, errs = rule.transformArg(md, 0, transformationCache)
arg, errs = rule.transformArg(md, transformationCache)
if errs != nil {
t.Fatalf("Unexpected errors executing transformations: %v", errs)
}
Expand All @@ -656,7 +656,7 @@ func TestTransformArgNoCacheForTXVariable(t *testing.T) {
}
rule := NewRule()
_ = rule.AddTransformation("AppendA", transformationAppendA)
arg, errs := rule.transformArg(md, 0, transformationCache)
arg, errs := rule.transformArg(md, transformationCache)
if errs != nil {
t.Fatalf("Unexpected errors executing transformations: %v", errs)
}
Expand Down Expand Up @@ -686,7 +686,7 @@ func TestTransformArgPrefixSharing(t *testing.T) {
_ = rule2.AddTransformation("AppendB", transformationAppendB)

// Evaluate rule1 first — caches the AppendA intermediate
arg1, errs := rule1.transformArg(md, 0, transformationCache)
arg1, errs := rule1.transformArg(md, transformationCache)
if errs != nil {
t.Fatalf("Unexpected errors: %v", errs)
}
Expand All @@ -698,7 +698,7 @@ func TestTransformArgPrefixSharing(t *testing.T) {
}

// Evaluate rule2 — should reuse the cached AppendA result and only compute AppendB
arg2, errs := rule2.transformArg(md, 0, transformationCache)
arg2, errs := rule2.transformArg(md, transformationCache)
if errs != nil {
t.Fatalf("Unexpected errors: %v", errs)
}
Expand All @@ -711,6 +711,25 @@ func TestTransformArgPrefixSharing(t *testing.T) {
}
}

// Not a profile: whether a profile reaches the collision depends on map iteration order.
func TestTransformArgRepeatedKeyDifferentValues(t *testing.T) {
transformationCache := map[transformationKey]transformationValue{}
// Repeated keys share one key string, as when args are added from a map[string][]string.
key := "q"
benign := &corazarules.MatchData{Variable_: variables.ArgsGet, Key_: key, Value_: "benign"}
evil := &corazarules.MatchData{Variable_: variables.ArgsGet, Key_: key, Value_: "evil"}

rule := NewRule()
_ = rule.AddTransformation("AppendA", transformationAppendA)

if arg, _ := rule.transformArg(benign, transformationCache); arg != "benignA" {
t.Errorf("Expected \"benignA\", got %q", arg)
}
if arg, _ := rule.transformArg(evil, transformationCache); arg != "evilA" {
t.Errorf("Expected \"evilA\", got %q (cached result of another value)", arg)
}
}

func TestClearTransformationsResetsID(t *testing.T) {
transformationCache := map[transformationKey]transformationValue{}
md := &corazarules.MatchData{
Expand All @@ -730,8 +749,8 @@ func TestClearTransformationsResetsID(t *testing.T) {
_ = ruleB.AddTransformation("AppendA", transformationAppendA)
_ = ruleB.AddTransformation("AppendB", transformationAppendB)

argA, _ := ruleA.transformArg(md, 0, transformationCache)
argB, _ := ruleB.transformArg(md, 0, transformationCache)
argA, _ := ruleA.transformArg(md, transformationCache)
argB, _ := ruleB.transformArg(md, transformationCache)

if argA != "testB" {
t.Errorf("Rule A (t:none resets): expected \"testB\", got %q", argA)
Expand Down
19 changes: 10 additions & 9 deletions internal/corazawaf/rulegroup.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,15 +288,16 @@ func NewRuleGroup() RuleGroup {
}

type transformationKey struct {
// TODO(anuraaga): This is a big hack to support performance on TinyGo. TinyGo
// cannot efficiently compute a hashcode for a struct if it has embedded non-fixed
// size fields, for example string as we'd prefer to use here. A pointer is usable,
// and it works for us since we know that the arg key string is populated once per
// transaction phase and we would never have different string pointers with the same
// content, or more problematically same pointer for different content, as the strings
// will be alive throughout the phase.
argKey *byte
argIndex int
// Transformations are a pure function of the value, so the cache is keyed on
// the value itself. A pointer is used instead of the string because TinyGo
// cannot efficiently compute a hashcode for a struct with non-fixed size fields
// such as string. The pointer also keeps the value alive, so its address cannot
// be reused for different content while the entry is cached. The length is part
// of the key because two substrings of one buffer can share a start address.
// The argument key and its position must not be used: repeated keys share the
// same key string, and collection iteration order is not stable.
argValue *byte
argValueLen int
argVariable variables.RuleVariable
transformationsID int
}
Expand Down
Loading