Skip to content

Commit e75c425

Browse files
authored
feat: v4.0.0, [email protected] (#119)
This upgrades go-libddwaf to v4.0.0, introducing libddwaf 1.24.1 and the new `Builder` pattern for maintaing WAF configuration and vending Handle instances. This gets rid of older, deprecated APIs while introducing new ones. This is a breaking upgrade from v3.
1 parent 05d6c88 commit e75c425

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+1814
-1496
lines changed

.github/workflows/_test_bare_metal.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,6 @@ jobs:
3333
run: ./.github/workflows/ci.sh
3434
env:
3535
DD_APPSEC_WAF_LOG_LEVEL: ${{ matrix.waf-log-level }}
36+
# We need a GITHUB_TOKEN in order to access the latest release of the AppSec rules from
37+
# the DataDog/appsec-rules repository.
38+
GITHUB_TOKEN: ${{ secrets.ACCESS_RULES_GITHUB_TOKEN }}

.github/workflows/test.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ jobs:
1616
bare-metal:
1717
name: GitHub Runner
1818
uses: ./.github/workflows/_test_bare_metal.yml
19+
# Needs secret access so it can access a GITHUB_TOKEN to verify the builder works with the
20+
# latest AppSec rules package.
21+
secrets: inherit
1922
containerized:
2023
name: Containerized
2124
uses: ./.github/workflows/_test_containerized.yml

README.md

Lines changed: 23 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ It consists of 2 separate entities: the bindings for the calls to libddwaf, and
66
An example usage would be:
77

88
```go
9-
import waf "github.com/DataDog/go-libddwaf/v3"
9+
import waf "github.com/DataDog/go-libddwaf/v4"
1010

1111
//go:embed
1212
var ruleset []byte
@@ -18,11 +18,19 @@ func main() {
1818
panic(err)
1919
}
2020

21-
wafHandle, err := waf.NewHandle(parsedRuleset, "", "")
21+
builder, err := waf.NewBuilder("", "")
22+
if err != nil {
23+
panic(err)
24+
}
25+
_, err := builder.AddOrUpdateConfig(parsedRuleset)
2226
if err != nil {
2327
panic(err)
2428
}
2529

30+
wafHandle := builder.Build()
31+
if wafHandle == nil {
32+
panic("WAF handle is nil")
33+
}
2634
defer wafHandle.Close()
2735

2836
wafCtx := wafHandle.NewContext()
@@ -36,7 +44,7 @@ func main() {
3644
}
3745
```
3846

39-
The API documentation details can be found on [pkg.go.dev](https://pkg.go.dev/github.com/DataDog/go-libddwaf/v3).
47+
The API documentation details can be found on [pkg.go.dev](https://pkg.go.dev/github.com/DataDog/go-libddwaf/v4).
4048

4149
Originally this project was only here to provide CGO Wrappers to the calls to libddwaf.
4250
But with the appearance of `ddwaf_object` tree like structure,
@@ -65,17 +73,18 @@ Note that:
6573

6674
The WAF bindings have multiple moving parts that are necessary to understand:
6775

68-
- Handle: a object wrapper over the pointer to the C WAF Handle
69-
- Context: a object wrapper over a pointer to the C WAF Context
76+
- `Builder`: an object wrapper over the pointer to the C WAF Builder
77+
- `Handle`: an object wrapper over the pointer to the C WAF Handle
78+
- `Context`: an object wrapper over a pointer to the C WAF Context
7079
- Encoder: its goal is to construct a tree of Waf Objects to send to the WAF
71-
- CGORefPool: Does all allocation operations for the construction of Waf Objects and keeps track of the equivalent go pointers
7280
- Decoder: Transforms Waf Objects returned from the WAF to usual go objects (e.g. maps, arrays, ...)
7381
- Library: The low-level go bindings to the C library, providing improved typing
7482

7583
```mermaid
7684
flowchart LR
85+
START:::hidden -->|NewBuilder| Builder -->|Build| Handle
7786
78-
START:::hidden -->|NewHandle| Handle -->|NewContext| Context
87+
Handle -->|NewContext| Context
7988
8089
Context -->|Encode Inputs| Encoder
8190
@@ -86,38 +95,23 @@ flowchart LR
8695
Handle -->|Decode Init Errors| Decoder
8796
8897
Context -->|Run| Library
89-
Context -->|Store Go References| CGORefPool
90-
91-
Encoder -->|Allocate Waf Objects| TempCGORefPool
92-
93-
TempCGORefPool -->|Copy after each encoding| CGORefPool
98+
Encoder -->|Allocate Waf Objects| runtime.Pinner
9499
95100
Library -->|Call C code| libddwaf
96101
97102
classDef hidden display: none;
98103
```
99104

100-
### CGO Reference Pool
101-
102-
The cgoRefPool type is a pure Go pointer pool of `ddwaf_object` C values on the Go memory heap.
103-
the `cgoRefPool` go type is a way to make sure we can safely send Go allocated data to the C side of the WAF
104-
The main issue is the following: the `WafObject` uses a C union to store the tree structure of the full object,
105-
union equivalent in go are interfaces and they are not compatible with C unions. The only way to be 100% sure
106-
that the Go `WafObject` struct has the same layout as the C one is to only use primitive types. So the only way to
107-
store a raw pointer is to use the `uintptr` type. But since `uintptr` do not have pointer semantics (and are just
108-
basically integers), we need another method to store the value as Go pointer because the GC will delete our data if it
109-
is not referenced by Go pointers.
110-
111-
That's where the `cgoRefPool` object comes into play: all new `WafObject` elements are created via this API which is especially
112-
built to make sure there is no gap for the Garbage Collector to exploit. From there, since underlying values of the
113-
`wafObject` are either arrays of WafObjects (for maps, structs and arrays) or string (for all ints, booleans and strings),
114-
we can store 2 slices of arrays and use `unsafe.KeepAlive` in each code path to protect them from the GC.
105+
### `runtime.Pinner`
115106

116-
All these objects stored in the reference pool need to live throughout the use of the associated Waf Context.
107+
When passing Go values to the WAF, it is necessary to make sure that memory remains valid and does
108+
not move until the WAF no longer has any pointers to it. We do this by using a `runtime.Pinner`.
109+
Persistent address data is added to a `Context`-associated `runtime.Pinner`; while ephemeral address
110+
data is managed by a transient `runtime.Pinner` that only exists for the duration of the call.
117111

118112
### Typical call to Run()
119113

120-
Here is an example of the flow of operations on a simple call to Run():
114+
Here is an example of the flow of operations on a simple call to `Run()`:
121115

122116
- Encode input data into WAF Objects and store references in the temporary pool
123117
- Lock the context mutex until the end of the call

alignement_test.go

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
// Purego only works on linux/macOS with amd64 and arm64 from now
77
//go:build (linux || darwin) && (amd64 || arm64) && !go1.25 && !datadog.no_waf && (cgo || appsec)
88

9-
package waf
9+
package libddwaf
1010

1111
import (
1212
"testing"
1313

14-
"github.com/DataDog/go-libddwaf/v3/internal/bindings"
15-
"github.com/DataDog/go-libddwaf/v3/internal/unsafe"
14+
"github.com/DataDog/go-libddwaf/v4/internal/bindings"
15+
"github.com/DataDog/go-libddwaf/v4/internal/unsafe"
1616

1717
"github.com/ebitengine/purego"
1818
"github.com/stretchr/testify/require"
@@ -31,52 +31,52 @@ func TestWafObject(t *testing.T) {
3131
lib := wafLib.Handle()
3232

3333
t.Run("invalid", func(t *testing.T) {
34-
var actual bindings.WafObject
35-
expected := bindings.WafObject{
36-
Type: bindings.WafInvalidType,
34+
var actual bindings.WAFObject
35+
expected := bindings.WAFObject{
36+
Type: bindings.WAFInvalidType,
3737
}
3838
r1, _, _ := purego.SyscallN(getSymbol(t, lib, "ddwaf_object_invalid"), unsafe.PtrToUintptr(&actual))
3939
require.NotEqualValues(t, 0, r1)
4040
require.Equal(t, expected, actual)
4141
})
4242

4343
t.Run("int", func(t *testing.T) {
44-
var actual bindings.WafObject
44+
var actual bindings.WAFObject
4545
r1, _, _ := purego.SyscallN(getSymbol(t, lib, "ddwaf_object_signed"), unsafe.PtrToUintptr(&actual), 42)
4646
require.NotEqualValues(t, 0, r1)
4747
require.EqualValues(t, 42, actual.Value)
48-
require.EqualValues(t, bindings.WafIntType, actual.Type)
48+
require.EqualValues(t, bindings.WAFIntType, actual.Type)
4949
})
5050

5151
t.Run("uint", func(t *testing.T) {
52-
var actual bindings.WafObject
52+
var actual bindings.WAFObject
5353
r1, _, _ := purego.SyscallN(getSymbol(t, lib, "ddwaf_object_unsigned"), unsafe.PtrToUintptr(&actual), 42)
5454
require.NotEqualValues(t, 0, r1)
5555
require.EqualValues(t, 42, actual.Value)
56-
require.EqualValues(t, bindings.WafUintType, actual.Type)
56+
require.EqualValues(t, bindings.WAFUintType, actual.Type)
5757
})
5858

5959
t.Run("string", func(t *testing.T) {
60-
var actual bindings.WafObject
61-
r1, _, _ := purego.SyscallN(getSymbol(t, lib, "ddwaf_object_string"), unsafe.PtrToUintptr[bindings.WafObject](&actual), unsafe.PtrToUintptr[byte](unsafe.Cstring("toto")))
60+
var actual bindings.WAFObject
61+
r1, _, _ := purego.SyscallN(getSymbol(t, lib, "ddwaf_object_string"), unsafe.PtrToUintptr(&actual), unsafe.PtrToUintptr(unsafe.Cstring("toto")))
6262
require.NotEqualValues(t, 0, r1)
6363
require.Equal(t, "toto", unsafe.Gostring(unsafe.Cast[byte](actual.Value)))
64-
require.EqualValues(t, bindings.WafStringType, actual.Type)
64+
require.EqualValues(t, bindings.WAFStringType, actual.Type)
6565
})
6666

6767
t.Run("padding", func(t *testing.T) {
68-
var actual [3]bindings.WafObject
68+
var actual [3]bindings.WAFObject
6969
r1, _, _ := purego.SyscallN(getSymbol(t, lib, "ddwaf_object_string"), unsafe.PtrToUintptr(&actual[0]), unsafe.PtrToUintptr(unsafe.Cstring("toto1")))
7070
require.NotEqualValues(t, 0, r1)
7171
require.Equal(t, "toto1", unsafe.Gostring(unsafe.Cast[byte](actual[0].Value)))
72-
require.EqualValues(t, bindings.WafStringType, actual[0].Type)
72+
require.EqualValues(t, bindings.WAFStringType, actual[0].Type)
7373
r1, _, _ = purego.SyscallN(getSymbol(t, lib, "ddwaf_object_string"), unsafe.PtrToUintptr(&actual[1]), unsafe.PtrToUintptr(unsafe.Cstring("toto2")))
7474
require.NotEqualValues(t, 0, r1)
7575
require.Equal(t, "toto2", unsafe.Gostring(unsafe.Cast[byte](actual[1].Value)))
76-
require.EqualValues(t, bindings.WafStringType, actual[1].Type)
76+
require.EqualValues(t, bindings.WAFStringType, actual[1].Type)
7777
r1, _, _ = purego.SyscallN(getSymbol(t, lib, "ddwaf_object_string"), unsafe.PtrToUintptr(&actual[2]), unsafe.PtrToUintptr(unsafe.Cstring("toto3")))
7878
require.NotEqualValues(t, 0, r1)
7979
require.Equal(t, "toto3", unsafe.Gostring(unsafe.Cast[byte](actual[2].Value)))
80-
require.EqualValues(t, bindings.WafStringType, actual[2].Type)
80+
require.EqualValues(t, bindings.WAFStringType, actual[2].Type)
8181
})
8282
}

builder.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2016-present Datadog, Inc.
5+
6+
package libddwaf
7+
8+
import (
9+
"errors"
10+
"fmt"
11+
"runtime"
12+
13+
"github.com/DataDog/go-libddwaf/v4/internal/bindings"
14+
)
15+
16+
// Builder manages an evolving WAF configuration over time. Its lifecycle is
17+
// typically tied to that of a remote configuration client, as its purpose is to
18+
// keep an up-to-date view of the current coniguration with low overhead. This
19+
// type is not safe for concurrent use, and users should protect it with a mutex
20+
// or similar when sharing it across multiple goroutines. All methods of this
21+
// type are safe to call with a nil receiver.
22+
type Builder struct {
23+
handle bindings.WAFBuilder
24+
}
25+
26+
// NewBuilder creates a new [Builder] instance. Its lifecycle is typically tied
27+
// to that of a remote configuration client, as its purpose is to keep an
28+
// up-to-date view of the current coniguration with low overhead. Returns nil if
29+
// an error occurs when initializing the builder. The caller is responsible for
30+
// calling [Builder.Close] when the builder is no longer needed.
31+
func NewBuilder(keyObfuscatorRegex string, valueObfuscatorRegex string) (*Builder, error) {
32+
if ok, err := Load(); !ok {
33+
return nil, err
34+
}
35+
36+
var pinner runtime.Pinner
37+
defer pinner.Unpin()
38+
hdl := wafLib.BuilderInit(newConfig(&pinner, keyObfuscatorRegex, valueObfuscatorRegex))
39+
40+
if hdl == 0 {
41+
return nil, errors.New("failed to initialize the WAF builder")
42+
}
43+
44+
return &Builder{handle: hdl}, nil
45+
}
46+
47+
// Close releases all resources associated with this builder.
48+
func (b *Builder) Close() {
49+
if b == nil || b.handle == 0 {
50+
return
51+
}
52+
wafLib.BuilderDestroy(b.handle)
53+
b.handle = 0
54+
}
55+
56+
var (
57+
errUpdateFailed = errors.New("failed to update WAF Builder instance")
58+
errBuilderClosed = errors.New("builder has already been closed")
59+
)
60+
61+
// AddOrUpdateConfig adds or updates a configuration fragment to this [Builder].
62+
// Returns the [Diagnostics] produced by adding or updating this configuration.
63+
func (b *Builder) AddOrUpdateConfig(path string, fragment any) (Diagnostics, error) {
64+
if b == nil || b.handle == 0 {
65+
return Diagnostics{}, errBuilderClosed
66+
}
67+
68+
if path == "" {
69+
return Diagnostics{}, errors.New("path cannot be blank")
70+
}
71+
72+
var pinner runtime.Pinner
73+
defer pinner.Unpin()
74+
75+
encoder := newMaxEncoder(&pinner)
76+
frag, err := encoder.Encode(fragment)
77+
if err != nil {
78+
return Diagnostics{}, fmt.Errorf("could not encode the config fragment into a WAF object; %w", err)
79+
}
80+
81+
var diagnosticsWafObj bindings.WAFObject
82+
defer wafLib.ObjectFree(&diagnosticsWafObj)
83+
84+
res := wafLib.BuilderAddOrUpdateConfig(b.handle, path, frag, &diagnosticsWafObj)
85+
86+
var diags Diagnostics
87+
if !diagnosticsWafObj.IsInvalid() {
88+
// The Diagnostics object will be invalid if the config was completely
89+
// rejected.
90+
diags, err = decodeDiagnostics(&diagnosticsWafObj)
91+
if err != nil {
92+
return diags, fmt.Errorf("failed to decode WAF diagnostics: %w", err)
93+
}
94+
}
95+
96+
if !res {
97+
return diags, errUpdateFailed
98+
}
99+
return diags, nil
100+
}
101+
102+
// RemoveConfig removes the configuration associated with the given path from
103+
// this [Builder]. Returns true if the removal was successful.
104+
func (b *Builder) RemoveConfig(path string) bool {
105+
if b == nil || b.handle == 0 {
106+
return false
107+
}
108+
109+
return wafLib.BuilderRemoveConfig(b.handle, path)
110+
}
111+
112+
// ConfigPaths returns the list of currently loaded configuration paths.
113+
func (b *Builder) ConfigPaths(filter string) []string {
114+
if b == nil || b.handle == 0 {
115+
return nil
116+
}
117+
118+
return wafLib.BuilderGetConfigPaths(b.handle, filter)
119+
}
120+
121+
// Build creates a new [Handle] instance that uses the current configuration.
122+
// Returns nil if an error occurs when building the handle. The caller is
123+
// responsible for calling [Handle.Close] when the handle is no longer needed.
124+
// This function may return nil.
125+
func (b *Builder) Build() *Handle {
126+
if b == nil || b.handle == 0 {
127+
return nil
128+
}
129+
130+
hdl := wafLib.BuilderBuildInstance(b.handle)
131+
if hdl == 0 {
132+
return nil
133+
}
134+
135+
return wrapHandle(hdl)
136+
}

0 commit comments

Comments
 (0)