Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lib): use memfd on linux instead of dumping libddwaf.so in /tmp #106

Merged
merged 2 commits into from
Sep 4, 2024
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
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,14 @@ Here is an example of the flow of operations on a simple call to Run():

This library uses [purego](https://github.com/ebitengine/purego) to implement C bindings without requiring use of CGO at compilation time. The high-level workflow
is to embed the C shared library using `go:embed`, dump it into a file, open the library using `dlopen`, load the
symbols using `dlsym`, and finally call them.
symbols using `dlsym`, and finally call them. On Linux systems, using `memfd_create(2)` enables the library to be loaded without
writing to the filesystem.

> :warning: Keep in mind that **purego only works on linux/darwin for amd64/arm64 and so does go-libddwaf.**

Another requirement of `libddwaf` is to have a FHS filesystem on your machine and, for linux, to provide `libc.so.6`,
Another requirement of `libddwaf` is to have a FHS filesystem on your machine and, for Linux, to provide `libc.so.6`,
`libpthread.so.0`, and `libdl.so.2` as dynamic libraries.

> :warning: Keep in mind that **purego only works on linux/darwin for amd64/arm64 and so does go-libddwaf.**

## Contributing pitfalls

- Cannot dlopen twice in the app lifetime on OSX. It messes with Thread Local Storage and usually finishes with a `std::bad_alloc()`
Expand Down
6 changes: 0 additions & 6 deletions _tools/libddwaf-updater/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,6 @@ func createEmbedSource(tgt target) {
"",
tgt.embedSourceDirective(),
"var libddwaf []byte",
"",
tgt.tempFilePatternStatement(),
"", // Trailing new line...
},
"\n",
Expand Down Expand Up @@ -315,10 +313,6 @@ func (t target) buildConstraintDirective() string {
return fmt.Sprintf("//go:build %s && %s && !%s && !datadog.no_waf && (cgo || appsec)", t.os, t.arch, goVersionUnsupported)
}

func (t target) tempFilePatternStatement() string {
return fmt.Sprintf("const embedNamePattern = \"%s-*.%s\"", t.base, t.ext)
}

func (t target) embedSourceDirective() string {
return fmt.Sprintf("//go:embed %s", t.binaryLibName())
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ require (
github.com/ebitengine/purego v0.6.0-alpha.5
github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.8.4
golang.org/x/sys v0.16.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.16.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
16 changes: 8 additions & 8 deletions internal/bindings/waf_dl.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,22 @@ type wafSymbols struct {
run uintptr
}

// newWafDl loads the libddwaf shared library and resolves all tge relevant symbols.
// NewWafDl 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 NewWafDl() (dl *WafDl, err error) {
file, err := lib.DumpEmbeddedWAF()
path, closer, err := lib.DumpEmbeddedWAF()
if err != nil {
return
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we not ignore this err?

Suggested change
return
return nil, fmt.Errorf("write an embedded WAF library: %w", err)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah sh*t. Should have disabled auto-merge when I saw your comments. Don't worry I will do it in another PR

}
defer func() {
if rmErr := os.Remove(file); rmErr != nil {
err = errors.Join(err, fmt.Errorf("error removing %s: %w", file, rmErr))
if rmErr := closer(); rmErr != nil {
err = errors.Join(err, fmt.Errorf("error removing %s: %w", path, rmErr))
}
}()

var handle uintptr
if handle, err = purego.Dlopen(file, purego.RTLD_GLOBAL|purego.RTLD_NOW); err != nil {
if handle, err = purego.Dlopen(path, purego.RTLD_GLOBAL|purego.RTLD_NOW); err != nil {
return
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
return
return nil, fmt.Errorf("dlopen: %w", err)

}

Expand Down Expand Up @@ -97,12 +97,12 @@ func (waf *WafDl) Close() error {
return purego.Dlclose(waf.handle)
}

// wafGetVersion returned string is a static string so we do not need to free it
// WafGetVersion returned string is a static string so we do not need to free it
func (waf *WafDl) WafGetVersion() string {
return unsafe.Gostring(unsafe.Cast[byte](waf.syscall(waf.getVersion)))
}

// wafInit initializes a new WAF with the provided ruleset, configuration and info objects. A
// WafInit initializes a new WAF with the provided ruleset, configuration and info objects. A
// cgoRefPool ensures that the provided input values are not moved or garbage collected by the Go
// runtime during the WAF call.
func (waf *WafDl) WafInit(ruleset *WafObject, config *WafConfig, info *WafObject) WafHandle {
Expand All @@ -125,7 +125,7 @@ func (waf *WafDl) WafDestroy(handle WafHandle) {
unsafe.KeepAlive(handle)
}

// wafKnownAddresses returns static strings so we do not need to free them
// WafKnownAddresses returns static strings so we do not need to free them
func (waf *WafDl) WafKnownAddresses(handle WafHandle) []string {
var nbAddresses uint32

Expand Down
13 changes: 6 additions & 7 deletions internal/bindings/waf_dl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"debug/elf"
"debug/macho"
"fmt"
"os"
"runtime"
"testing"

Expand All @@ -33,14 +32,14 @@ func TestVerifyHeader(t *testing.T) {
// testVerifyELFHeader is here to ease the debug cases that will likely need
// to dive in the linker to debug because the error handling is very poor
func testVerifyELFHeader(t *testing.T) {
file, err := lib.DumpEmbeddedWAF()
path, closer, err := lib.DumpEmbeddedWAF()
require.NoError(t, err)

defer func() {
_ = os.Remove(file)
_ = closer()
}()

elfFile, err := elf.Open(file)
elfFile, err := elf.Open(path)
require.NoError(t, err)

switch runtime.GOARCH {
Expand All @@ -60,14 +59,14 @@ func testVerifyELFHeader(t *testing.T) {
// testVerifyMachOHeader is here to ease the debug cases that will likely need
// to dive in the linker to debug because the error handling is very poor
func testVerifyMachOHeader(t *testing.T) {
file, err := lib.DumpEmbeddedWAF()
path, closer, err := lib.DumpEmbeddedWAF()
require.NoError(t, err)

defer func() {
_ = os.Remove(file)
_ = closer()
}()

machOFile, err := macho.Open(file)
machOFile, err := macho.Open(path)
require.NoError(t, err)

switch runtime.GOARCH {
Expand Down
57 changes: 57 additions & 0 deletions internal/lib/dump_waf_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// 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.

//go:build darwin && (amd64 || arm64) && !go1.24 && !datadog.no_waf && (cgo || appsec)

package lib

import (
"bytes"
"compress/gzip"
_ "embed"
"errors"
"fmt"
"io"
"os"
)

// DumpEmbeddedWAF for darwin platform.
// DumpEmbeddedWAF creates a temporary file with the embedded WAF library content and returns the path to the file,
// a closer function and an error. This is the only way to make all implementations of DumpEmbeddedWAF consistent
// across all platforms.
func DumpEmbeddedWAF() (path string, closer func() error, err error) {
file, err := os.CreateTemp("", "libddwaf-*.dylib")
if err != nil {
return "", nil, fmt.Errorf("error creating temp file: %w", err)
}

defer func() {
if err != nil {
if closeErr := file.Close(); closeErr != nil {
err = errors.Join(err, fmt.Errorf("error closing file: %w", closeErr))
}
if rmErr := os.Remove(file.Name()); rmErr != nil {
err = errors.Join(err, fmt.Errorf("error removing file: %w", rmErr))
}
}
}()

gr, err := gzip.NewReader(bytes.NewReader(libddwaf))
if err != nil {
return "", nil, fmt.Errorf("error creating gzip reader: %w", err)
}

if _, err := io.Copy(file, gr); err != nil {
eliottness marked this conversation as resolved.
Show resolved Hide resolved
eliottness marked this conversation as resolved.
Show resolved Hide resolved
return "", nil, fmt.Errorf("error copying gzip content to file: %w", err)
}

if err := gr.Close(); err != nil {
return "", nil, fmt.Errorf("error closing gzip reader: %w", err)
}

return file.Name(), func() error {
return errors.Join(file.Close(), os.Remove(file.Name()))
}, nil
}
58 changes: 58 additions & 0 deletions internal/lib/dump_waf_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// 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.

//go:build linux && (amd64 || arm64) && !go1.24 && !datadog.no_waf && (cgo || appsec)

package lib

import (
"bytes"
"compress/gzip"
"errors"
"fmt"
"io"
"os"

"golang.org/x/sys/unix"
)

// DumpEmbeddedWAF for linux systems.
// It creates a memfd and writes the embedded WAF library to it. Then it returns the path the /proc/self/fd/<fd> path
// to the file. This trick makes us able to load the library without having to write it to disk.
// Hence, making go-libddwaf work on full read-only filesystems.
func DumpEmbeddedWAF() (path string, closer func() error, err error) {
fd, err := unix.MemfdCreate("libddwaf", 0)
if err != nil {
return "", nil, fmt.Errorf("error creating memfd: %w", err)
}

file := os.NewFile(uintptr(fd), fmt.Sprintf("/proc/self/fd/%d", fd))
if file == nil {
return "", nil, errors.New("error creating file from fd")
}

defer func() {
if file != nil && err != nil {
if closeErr := file.Close(); closeErr != nil {
err = errors.Join(err, fmt.Errorf("error closing file: %w", closeErr))
}
}
}()

gr, err := gzip.NewReader(bytes.NewReader(libddwaf))
if err != nil {
return "", nil, fmt.Errorf("error creating gzip reader: %w", err)
}

if _, err := io.Copy(file, gr); err != nil {
eliottness marked this conversation as resolved.
Show resolved Hide resolved
eliottness marked this conversation as resolved.
Show resolved Hide resolved
return "", nil, fmt.Errorf("error copying gzip content to memfd: %w", err)
}

if err := gr.Close(); err != nil {
return "", nil, fmt.Errorf("error closing gzip reader: %w", err)
}

return file.Name(), file.Close, nil
}
61 changes: 0 additions & 61 deletions internal/lib/lib.go

This file was deleted.

2 changes: 0 additions & 2 deletions internal/lib/lib_darwin_amd64.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,3 @@ import _ "embed" // Needed for go:embed

//go:embed libddwaf-darwin-amd64.dylib.gz
var libddwaf []byte

const embedNamePattern = "libddwaf-*.dylib"
2 changes: 0 additions & 2 deletions internal/lib/lib_darwin_arm64.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,3 @@ import _ "embed" // Needed for go:embed

//go:embed libddwaf-darwin-arm64.dylib.gz
var libddwaf []byte

const embedNamePattern = "libddwaf-*.dylib"
2 changes: 0 additions & 2 deletions internal/lib/lib_linux_amd64.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,3 @@ import _ "embed" // Needed for go:embed

//go:embed libddwaf-linux-amd64.so.gz
var libddwaf []byte

const embedNamePattern = "libddwaf-*.so"
2 changes: 0 additions & 2 deletions internal/lib/lib_linux_arm64.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,3 @@ import _ "embed" // Needed for go:embed

//go:embed libddwaf-linux-arm64.so.gz
var libddwaf []byte

const embedNamePattern = "libddwaf-*.so"
13 changes: 13 additions & 0 deletions internal/lib/version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// 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 2024 Datadog, Inc.

package lib

import (
_ "embed" // For go:embed
)

//go:embed .version
var EmbeddedWAFVersion string
Loading