Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/_test_bare_metal.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
strategy:
fail-fast: false
matrix:
runs-on: [ macos-15, macos-14, macos-13, ubuntu-24.04, ubuntu-22.04, windows-latest, arm-4core-linux ]
runs-on: [ macos-26, macos-latest-large, macos-13, ubuntu-24.04, ubuntu-22.04, windows-latest, ubuntu-24.04-arm ]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

macos-latest-large is the only amd64 image available under macos now

go-version: ${{ fromJson(needs.go-versions-matrix.outputs.json) }}
include:
# Test with DD_APPSEC_WAF_LOG_LEVEL (only latest go version)
Expand Down
19 changes: 2 additions & 17 deletions .github/workflows/_test_containerized.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ jobs:
image:
# Standard golang images
- golang:{0}-alpine
- golang:{0}-trixie
- golang:{0}-bookworm
- golang:{0}-bullseye
go-version:
- ${{ needs.go-versions.outputs.stable }}
- ${{ needs.go-versions.outputs.oldstable }}
Expand All @@ -41,23 +41,8 @@ jobs:
waf-log-level: TRACE
name: ${{ matrix.arch }} ${{ format(matrix.image, matrix.go-version) }} go${{ matrix.go-version }}${{ matrix.waf-log-level && format(' (DD_APPSEC_WAF_LOG_LEVEL={0})', matrix.waf-log-level) || '' }}
# We use ARM runners when needed to avoid the performance hit of QEMU
runs-on: ${{ matrix.arch == 'amd64' && 'ubuntu-latest' || 'arm-4core-linux' }}
runs-on: ${{ matrix.arch == 'amd64' && 'ubuntu-latest' || 'ubuntu-24.04-arm' }}
steps:
# Docker is not present on early-access ARM runners
- name: Prepare ARM Runner
if: matrix.arch == 'arm64'
run: |-
for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove -y $pkg || echo "Not present: $pkg"; done

sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL "https://download.docker.com/linux/ubuntu/gpg" -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io

- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
Expand Down
2 changes: 1 addition & 1 deletion _tools/libddwaf-updater/update.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/bash

cd $(dirname $0)
cd "$(dirname "$0")" || exit
exec go run ./update.go "$@"
2 changes: 1 addition & 1 deletion alignement_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func TestWafObject(t *testing.T) {
_, err := Load()
require.NoError(t, err)

lib := gWafLib.Handle()
lib := bindings.Lib.Handle()

t.Run("invalid", func(t *testing.T) {
var actual bindings.WAFObject
Expand Down
22 changes: 10 additions & 12 deletions builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func NewBuilder(keyObfuscatorRegex string, valueObfuscatorRegex string) (*Builde

var pinner runtime.Pinner
defer pinner.Unpin()
hdl := gWafLib.BuilderInit(newConfig(&pinner, keyObfuscatorRegex, valueObfuscatorRegex))
hdl := bindings.Lib.BuilderInit(newConfig(&pinner, keyObfuscatorRegex, valueObfuscatorRegex))

if hdl == 0 {
return nil, errors.New("failed to initialize the WAF builder")
Expand All @@ -51,7 +51,7 @@ func (b *Builder) Close() {
if b == nil || b.handle == 0 {
return
}
gWafLib.BuilderDestroy(b.handle)
bindings.Lib.BuilderDestroy(b.handle)
b.handle = 0
}

Expand All @@ -65,15 +65,13 @@ const defaultRecommendedRulesetPath = "::/go-libddwaf/default/recommended.json"
// AddDefaultRecommendedRuleset adds the default recommended ruleset to the
// receiving [Builder], and returns the [Diagnostics] produced in the process.
func (b *Builder) AddDefaultRecommendedRuleset() (Diagnostics, error) {
var pinner runtime.Pinner
defer pinner.Unpin()

ruleset, err := ruleset.DefaultRuleset(&pinner)
defaultRuleset, err := ruleset.DefaultRuleset()
defer bindings.Lib.ObjectFree(&defaultRuleset)
if err != nil {
return Diagnostics{}, fmt.Errorf("failed to load default recommended ruleset: %w", err)
}

diag, err := b.addOrUpdateConfig(defaultRecommendedRulesetPath, &ruleset)
diag, err := b.addOrUpdateConfig(defaultRecommendedRulesetPath, &defaultRuleset)
if err == nil {
b.defaultLoaded = true
}
Expand Down Expand Up @@ -122,9 +120,9 @@ func (b *Builder) AddOrUpdateConfig(path string, fragment any) (Diagnostics, err
// Returns the [Diagnostics] produced by adding or updating this configuration.
func (b *Builder) addOrUpdateConfig(path string, cfg *bindings.WAFObject) (Diagnostics, error) {
var diagnosticsWafObj bindings.WAFObject
defer gWafLib.ObjectFree(&diagnosticsWafObj)
defer bindings.Lib.ObjectFree(&diagnosticsWafObj)

res := gWafLib.BuilderAddOrUpdateConfig(b.handle, path, cfg, &diagnosticsWafObj)
res := bindings.Lib.BuilderAddOrUpdateConfig(b.handle, path, cfg, &diagnosticsWafObj)

var diags Diagnostics
if !diagnosticsWafObj.IsInvalid() {
Expand All @@ -150,7 +148,7 @@ func (b *Builder) RemoveConfig(path string) bool {
return false
}

return gWafLib.BuilderRemoveConfig(b.handle, path)
return bindings.Lib.BuilderRemoveConfig(b.handle, path)
}

// ConfigPaths returns the list of currently loaded configuration paths.
Expand All @@ -159,7 +157,7 @@ func (b *Builder) ConfigPaths(filter string) []string {
return nil
}

return gWafLib.BuilderGetConfigPaths(b.handle, filter)
return bindings.Lib.BuilderGetConfigPaths(b.handle, filter)
}

// Build creates a new [Handle] instance that uses the current configuration.
Expand All @@ -171,7 +169,7 @@ func (b *Builder) Build() *Handle {
return nil
}

hdl := gWafLib.BuilderBuildInstance(b.handle)
hdl := bindings.Lib.BuilderBuildInstance(b.handle)
if hdl == 0 {
return nil
}
Expand Down
4 changes: 2 additions & 2 deletions builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,8 +297,8 @@ func TestBuilder(t *testing.T) {
require.NotEmpty(t, res.Events)
require.Equal(t,
map[string]any{"block_request": map[string]any{
"grpc_status_code": "10",
"status_code": "403",
"grpc_status_code": uint64(10),
"status_code": uint64(403),
"type": "auto",
}},
res.Actions,
Expand Down
6 changes: 3 additions & 3 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,12 +225,12 @@ func (context *Context) run(persistentData, ephemeralData *bindings.WAFObject, r

var result bindings.WAFObject
pinner.Pin(&result)
defer gWafLib.ObjectFree(&result)
defer bindings.Lib.ObjectFree(&result)

// The value of the timeout cannot exceed 2^55
// cf. https://en.cppreference.com/w/cpp/chrono/duration
timeout := uint64(runTimer.SumRemaining().Microseconds()) & 0x008FFFFFFFFFFFFF
ret := gWafLib.Run(context.cContext, persistentData, ephemeralData, &result, timeout)
ret := bindings.Lib.Run(context.cContext, persistentData, ephemeralData, &result, timeout)

decodeTimer := runTimer.MustLeaf(DecodeTimeKey)
decodeTimer.Start()
Expand Down Expand Up @@ -324,7 +324,7 @@ func (context *Context) Close() {
context.mutex.Lock()
defer context.mutex.Unlock()

gWafLib.ContextDestroy(context.cContext)
bindings.Lib.ContextDestroy(context.cContext)
defer context.handle.Close() // Reduce the reference counter of the Handle.
context.cContext = 0 // Makes it easy to spot use-after-free/double-free issues

Expand Down
8 changes: 4 additions & 4 deletions handle.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func (handle *Handle) NewContext(timerOptions ...timer.Option) (*Context, error)
return nil, fmt.Errorf("handle was released")
}

cContext := gWafLib.ContextInit(handle.cHandle)
cContext := bindings.Lib.ContextInit(handle.cHandle)
if cContext == 0 {
handle.Close() // We couldn't get a context, so we no longer have an implicit reference to the Handle in it...
return nil, fmt.Errorf("could not get C context")
Expand All @@ -77,13 +77,13 @@ func (handle *Handle) NewContext(timerOptions ...timer.Option) (*Context, error)
// Addresses returns the list of addresses the WAF has been configured to monitor based on the input
// ruleset.
func (handle *Handle) Addresses() []string {
return gWafLib.KnownAddresses(handle.cHandle)
return bindings.Lib.KnownAddresses(handle.cHandle)
}

// Actions returns the list of actions the WAF has been configured to monitor based on the input
// ruleset.
func (handle *Handle) Actions() []string {
return gWafLib.KnownActions(handle.cHandle)
return bindings.Lib.KnownActions(handle.cHandle)
}

// Close decrements the reference counter of this [Handle], possibly allowing it to be destroyed
Expand All @@ -95,7 +95,7 @@ func (handle *Handle) Close() {
return
}

gWafLib.Destroy(handle.cHandle)
bindings.Lib.Destroy(handle.cHandle)
handle.cHandle = 0 // Makes it easy to spot use-after-free/double-free issues
}

Expand Down
6 changes: 5 additions & 1 deletion internal/bindings/libddwaf.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@ type wafSymbols struct {
builderBuildInstance uintptr
builderGetConfigPaths uintptr
builderDestroy uintptr
setLogCb uintptr
destroy uintptr
knownAddresses uintptr
knownActions uintptr
getVersion uintptr
contextInit uintptr
contextDestroy uintptr
objectFree uintptr
objectFromJSON uintptr
run uintptr
setLogCb uintptr
}

// newWafSymbols resolves the symbols of [wafSymbols] from the provided
Expand Down Expand Up @@ -69,6 +70,9 @@ func newWafSymbols(handle uintptr) (syms wafSymbols, err error) {
if syms.objectFree, err = purego.Dlsym(handle, "ddwaf_object_free"); err != nil {
return syms, err
}
if syms.objectFromJSON, err = purego.Dlsym(handle, "ddwaf_object_from_json"); err != nil {
return syms, err
}
if syms.run, err = purego.Dlsym(handle, "ddwaf_run"); err != nil {
return syms, err
}
Expand Down
93 changes: 93 additions & 0 deletions internal/bindings/singleton.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016-present Datadog, Inc.

package bindings

import (
"errors"
"sync"

"github.com/DataDog/go-libddwaf/v4/internal/support"
)

// Globally dlopen() libddwaf only once because several dlopens (eg. in tests)
// aren't supported by macOS.
var (
// Lib is libddwaf's dynamic library handle and entrypoints. This is only safe to
// read after calling [Load] or having acquired [gMu].
Lib *WAFLib
// libddwaf's dlopen error if any. This is only safe to read after calling
// [Load] or having acquired [gMu].
gWafLoadErr error
// Protects the global variables above.
gMu sync.Mutex

openWafOnce sync.Once
)

// Load loads libddwaf's dynamic library. The dynamic library is opened only
// once by the first call to this function and internally stored globally.
// No function is currently provided in this API to unload it.
//
// This function is automatically called by [NewBuilder], and most users need
// not explicitly call it. It is however useful in order to explicitly check
// for the status of the Lib library's initialization.
//
// The function returns true when libddwaf was successfully loaded, along with
// an error value. An error might still be returned even though the Lib load was
// successful: in such cases the error is indicative that some non-critical
// features are not available; but the Lib may still be used.
func Load() (bool, error) {
if ok, err := Usable(); !ok {
return false, err
}

openWafOnce.Do(func() {
// Acquire the global state mutex so we don't have a race condition with
// [Usable] here.
gMu.Lock()
defer gMu.Unlock()

Lib, gWafLoadErr = newWAFLib()
if gWafLoadErr != nil {
return
}
wafVersion = Lib.GetVersion()
})

return Lib != nil, gWafLoadErr
}

var wafVersion string

// Version returns the version returned by libddwaf.
// It relies on the dynamic loading of the library, which can fail and return
// an empty string or the previously loaded version, if any.
func Version() string {
_, _ = Load()
return wafVersion
}

// Usable returns true if the Lib is usable, false and an error otherwise.
//
// If the Lib is usable, an error value may still be returned and should be
// treated as a warning (it is non-blocking).
//
// The following conditions are checked:
// - The Lib library has been loaded successfully (you need to call [Load] first for this case to be
// taken into account)
// - The Lib library has not been manually disabled with the `datadog.no_waf` go build tag
// - The Lib library is not in an unsupported OS/Arch
// - The Lib library is not in an unsupported Go version
func Usable() (bool, error) {
wafSupportErrors := errors.Join(support.WafSupportErrors()...)
wafManuallyDisabledErr := support.WafManuallyDisabledError()

// Acquire the global state mutex as we are not calling [Load] here, so we
// need to explicitly avoid a race condition with it.
gMu.Lock()
defer gMu.Unlock()
return (Lib != nil || gWafLoadErr == nil) && wafSupportErrors == nil && wafManuallyDisabledErr == nil, errors.Join(gWafLoadErr, wafSupportErrors, wafManuallyDisabledErr)
}
18 changes: 16 additions & 2 deletions internal/bindings/waf_dl.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ type WAFLib struct {
handle uintptr
}

// NewWAFLib loads the libddwaf shared library and resolves all tge relevant symbols.
// newWAFLib loads the libddwaf shared library and resolves all tge relevant symbols.
// The caller is responsible for calling wafDl.Close on the returned object once they
// are done with it so that associated resources can be released.
func NewWAFLib() (dl *WAFLib, err error) {
func newWAFLib() (dl *WAFLib, err error) {
path, closer, err := lib.DumpEmbeddedWAF()
if err != nil {
return nil, fmt.Errorf("dump embedded WAF: %w", err)
Expand Down Expand Up @@ -237,6 +237,20 @@ func (waf *WAFLib) Handle() uintptr {
return waf.handle
}

func (waf *WAFLib) ObjectFromJSON(json []byte) (WAFObject, bool) {
var (
obj WAFObject
pinner runtime.Pinner
)

defer pinner.Unpin()
pinner.Pin(&json)
pinner.Pin(&obj)

success := waf.syscall(waf.objectFromJSON, unsafe.PtrToUintptr(&obj), unsafe.SliceToUintptr(json), uintptr(len(json))) != 0
return obj, success
}

// syscall is the only way to make C calls with this interface.
// purego implementation limits the number of arguments to 9, it will panic if more are provided
// Note: `purego.SyscallN` has 3 return values: these are the following:
Expand Down
4 changes: 3 additions & 1 deletion internal/bindings/waf_dl_unsupported.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (

type WAFLib struct{}

func NewWAFLib() (*WAFLib, error) {
func newWAFLib() (*WAFLib, error) {
return nil, errors.New("go-libddwaf is not supported on this platform")
}

Expand Down Expand Up @@ -56,4 +56,6 @@ func (*WAFLib) Run(WAFContext, *WAFObject, *WAFObject, *WAFObject, uint64) WAFRe
return WAFErrInternal
}

func (waf *WAFLib) ObjectFromJSON(json []byte) (WAFObject, bool) { return WAFObject{}, false }

func (*WAFLib) Handle() uintptr { return 0 }
2 changes: 1 addition & 1 deletion internal/lib/.version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.25.1
1.28.1
Binary file modified internal/lib/libddwaf-darwin-amd64.dylib.gz
Binary file not shown.
Binary file modified internal/lib/libddwaf-darwin-arm64.dylib.gz
Binary file not shown.
Binary file modified internal/lib/libddwaf-linux-amd64.so.gz
Binary file not shown.
Binary file modified internal/lib/libddwaf-linux-arm64.so.gz
Binary file not shown.
Loading
Loading