Support syscall

This commit is contained in:
David 2025-10-17 17:46:05 +02:00 committed by GitHub
parent 05de0670ea
commit d1ab6ed489
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 247 additions and 8 deletions

View File

@ -131,14 +131,14 @@ THIS FILE MUST NOT BE EDITED BY HAND
| <a id="opt-experimental-localplugins-name-settings" href="#opt-experimental-localplugins-name-settings" title="#opt-experimental-localplugins-name-settings">experimental.localplugins._name_.settings</a> | Plugin's settings (works only for wasm plugins). | |
| <a id="opt-experimental-localplugins-name-settings-envs" href="#opt-experimental-localplugins-name-settings-envs" title="#opt-experimental-localplugins-name-settings-envs">experimental.localplugins._name_.settings.envs</a> | Environment variables to forward to the wasm guest. | |
| <a id="opt-experimental-localplugins-name-settings-mounts" href="#opt-experimental-localplugins-name-settings-mounts" title="#opt-experimental-localplugins-name-settings-mounts">experimental.localplugins._name_.settings.mounts</a> | Directory to mount to the wasm guest. | |
| <a id="opt-experimental-localplugins-name-settings-useunsafe" href="#opt-experimental-localplugins-name-settings-useunsafe" title="#opt-experimental-localplugins-name-settings-useunsafe">experimental.localplugins._name_.settings.useunsafe</a> | Allow the plugin to use unsafe package. | false |
| <a id="opt-experimental-localplugins-name-settings-useunsafe" href="#opt-experimental-localplugins-name-settings-useunsafe" title="#opt-experimental-localplugins-name-settings-useunsafe">experimental.localplugins._name_.settings.useunsafe</a> | Allow the plugin to use unsafe and syscall packages. | false |
| <a id="opt-experimental-otlplogs" href="#opt-experimental-otlplogs" title="#opt-experimental-otlplogs">experimental.otlplogs</a> | Enables the OpenTelemetry logs integration. | false |
| <a id="opt-experimental-plugins-name-hash" href="#opt-experimental-plugins-name-hash" title="#opt-experimental-plugins-name-hash">experimental.plugins._name_.hash</a> | plugin's hash to validate' | |
| <a id="opt-experimental-plugins-name-modulename" href="#opt-experimental-plugins-name-modulename" title="#opt-experimental-plugins-name-modulename">experimental.plugins._name_.modulename</a> | plugin's module name. | |
| <a id="opt-experimental-plugins-name-settings" href="#opt-experimental-plugins-name-settings" title="#opt-experimental-plugins-name-settings">experimental.plugins._name_.settings</a> | Plugin's settings (works only for wasm plugins). | |
| <a id="opt-experimental-plugins-name-settings-envs" href="#opt-experimental-plugins-name-settings-envs" title="#opt-experimental-plugins-name-settings-envs">experimental.plugins._name_.settings.envs</a> | Environment variables to forward to the wasm guest. | |
| <a id="opt-experimental-plugins-name-settings-mounts" href="#opt-experimental-plugins-name-settings-mounts" title="#opt-experimental-plugins-name-settings-mounts">experimental.plugins._name_.settings.mounts</a> | Directory to mount to the wasm guest. | |
| <a id="opt-experimental-plugins-name-settings-useunsafe" href="#opt-experimental-plugins-name-settings-useunsafe" title="#opt-experimental-plugins-name-settings-useunsafe">experimental.plugins._name_.settings.useunsafe</a> | Allow the plugin to use unsafe package. | false |
| <a id="opt-experimental-plugins-name-settings-useunsafe" href="#opt-experimental-plugins-name-settings-useunsafe" title="#opt-experimental-plugins-name-settings-useunsafe">experimental.plugins._name_.settings.useunsafe</a> | Allow the plugin to use unsafe and syscall packages. | false |
| <a id="opt-experimental-plugins-name-version" href="#opt-experimental-plugins-name-version" title="#opt-experimental-plugins-name-version">experimental.plugins._name_.version</a> | plugin's version. | |
| <a id="opt-global-checknewversion" href="#opt-global-checknewversion" title="#opt-global-checknewversion">global.checknewversion</a> | Periodically check if a new version has been released. | true |
| <a id="opt-global-sendanonymoususage" href="#opt-global-sendanonymoususage" title="#opt-global-sendanonymoususage">global.sendanonymoususage</a> | Periodically send anonymous usage statistics. If the option is not specified, it will be disabled by default. | false |

View File

@ -367,7 +367,7 @@ Environment variables to forward to the wasm guest.
Directory to mount to the wasm guest.
`--experimental.localplugins.<name>.settings.useunsafe`:
Allow the plugin to use unsafe package. (Default: ```false```)
Allow the plugin to use unsafe and syscall packages. (Default: ```false```)
`--experimental.otlplogs`:
Enables the OpenTelemetry logs integration. (Default: ```false```)
@ -385,7 +385,7 @@ Environment variables to forward to the wasm guest.
Directory to mount to the wasm guest.
`--experimental.plugins.<name>.settings.useunsafe`:
Allow the plugin to use unsafe package. (Default: ```false```)
Allow the plugin to use unsafe and syscall packages. (Default: ```false```)
`--experimental.plugins.<name>.version`:
plugin's version.

View File

@ -367,7 +367,7 @@ Environment variables to forward to the wasm guest.
Directory to mount to the wasm guest.
`TRAEFIK_EXPERIMENTAL_LOCALPLUGINS_<NAME>_SETTINGS_USEUNSAFE`:
Allow the plugin to use unsafe package. (Default: ```false```)
Allow the plugin to use unsafe and syscall packages. (Default: ```false```)
`TRAEFIK_EXPERIMENTAL_OTLPLOGS`:
Enables the OpenTelemetry logs integration. (Default: ```false```)
@ -385,7 +385,7 @@ Environment variables to forward to the wasm guest.
Directory to mount to the wasm guest.
`TRAEFIK_EXPERIMENTAL_PLUGINS_<NAME>_SETTINGS_USEUNSAFE`:
Allow the plugin to use unsafe package. (Default: ```false```)
Allow the plugin to use unsafe and syscall packages. (Default: ```false```)
`TRAEFIK_EXPERIMENTAL_PLUGINS_<NAME>_VERSION`:
plugin's version.

View File

@ -0,0 +1,3 @@
module testpluginsafe
go 1.23.0

View File

@ -0,0 +1,21 @@
package testpluginsafe
import (
"context"
"net/http"
)
type Config struct {
Message string
}
func CreateConfig() *Config {
return &Config{Message: "safe plugin"}
}
func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Header().Set("X-Test-Plugin", "safe")
next.ServeHTTP(rw, req)
}), nil
}

View File

@ -0,0 +1,3 @@
module testpluginsyscall
go 1.23.0

View File

@ -0,0 +1,29 @@
package testpluginsyscall
import (
"context"
"net/http"
"syscall"
"unsafe"
)
type Config struct {
Message string
}
func CreateConfig() *Config {
return &Config{Message: "syscall plugin"}
}
func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) {
// Use syscall and unsafe to test they're available
pid := syscall.Getpid()
size := unsafe.Sizeof(int(0))
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Header().Set("X-Test-Plugin", "syscall")
rw.Header().Set("X-Test-PID", string(rune(pid)))
rw.Header().Set("X-Test-Size", string(rune(size)))
next.ServeHTTP(rw, req)
}), nil
}

View File

@ -0,0 +1,3 @@
module testpluginunsafe
go 1.23.0

View File

@ -0,0 +1,26 @@
package testpluginunsafe
import (
"context"
"net/http"
"unsafe"
)
type Config struct {
Message string
}
func CreateConfig() *Config {
return &Config{Message: "unsafe only plugin"}
}
func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) {
// Use ONLY unsafe to test it's available
size := unsafe.Sizeof(int(0))
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Header().Set("X-Test-Plugin", "unsafe-only")
rw.Header().Set("X-Test-Unsafe-Size", string(rune(size)))
next.ServeHTTP(rw, req)
}), nil
}

View File

@ -16,6 +16,7 @@ import (
"github.com/traefik/traefik/v3/pkg/observability/logs"
"github.com/traefik/yaegi/interp"
"github.com/traefik/yaegi/stdlib"
"github.com/traefik/yaegi/stdlib/syscall"
"github.com/traefik/yaegi/stdlib/unsafe"
)
@ -135,7 +136,7 @@ func newInterpreter(ctx context.Context, goPath string, manifest *Manifest, sett
}
if manifest.UseUnsafe && !settings.UseUnsafe {
return nil, errors.New("this plugin uses unsafe import. If you want to use it, you need to allow useUnsafe in the settings")
return nil, errors.New("this plugin uses restricted imports. If you want to use it, you need to allow useUnsafe in the settings")
}
if settings.UseUnsafe && manifest.UseUnsafe {
@ -143,6 +144,11 @@ func newInterpreter(ctx context.Context, goPath string, manifest *Manifest, sett
if err != nil {
return nil, fmt.Errorf("failed to load unsafe symbols: %w", err)
}
err = i.Use(syscall.Symbols)
if err != nil {
return nil, fmt.Errorf("failed to load syscall symbols: %w", err)
}
}
err = i.Use(ppSymbols())

View File

@ -0,0 +1,148 @@
package plugins
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/yaegi/interp"
)
// TestNewInterpreter_SyscallErrorCase - Tests the security gate logic
func TestNewInterpreter_SyscallErrorCase(t *testing.T) {
manifest := &Manifest{
Import: "does-not-matter-will-error-before-import",
UseUnsafe: true, // Plugin wants unsafe access
}
settings := Settings{
UseUnsafe: false, // But admin doesn't allow it
}
ctx := t.Context()
_, err := newInterpreter(ctx, "/tmp", manifest, settings)
// This proves our security gate logic works
require.Error(t, err)
assert.Contains(t, err.Error(), "restricted imports", "Our error message should be returned")
}
// TestNewYaegiMiddlewareBuilder_WithSyscallSupport - Tests the ACTUAL production code!
func TestNewYaegiMiddlewareBuilder_WithSyscallSupport(t *testing.T) {
tests := []struct {
name string
pluginType string
manifestUnsafe bool
settingsUnsafe bool
shouldSucceed bool
expectedError string
}{
{
name: "Should work with safe plugin when useUnsafe disabled",
pluginType: "safe",
manifestUnsafe: false,
settingsUnsafe: false,
shouldSucceed: true,
},
{
name: "Should work with unsafe-only plugin when useUnsafe enabled",
pluginType: "unsafe-only",
manifestUnsafe: true,
settingsUnsafe: true,
shouldSucceed: true,
},
{
name: "Should work with unsafe+syscall plugin when useUnsafe enabled",
pluginType: "unsafe+syscall",
manifestUnsafe: true,
settingsUnsafe: true,
shouldSucceed: true,
},
{
name: "Should fail when plugin needs unsafe but setting disabled",
pluginType: "unsafe-only",
manifestUnsafe: true,
settingsUnsafe: false,
shouldSucceed: false,
expectedError: "restricted imports",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
ctx := t.Context()
// Set GOPATH to include our fixtures directory
goPath := "fixtures"
// Create interpreter using our ACTUAL newInterpreter function
// This will automatically import the real test plugin!
interpreter, err := createInterpreterForTesting(ctx, goPath, tc.pluginType, tc.manifestUnsafe, tc.settingsUnsafe)
if tc.shouldSucceed {
require.NoError(t, err)
require.NotNil(t, interpreter)
// Test actual middleware building using newYaegiMiddlewareBuilder
// The plugin is already loaded by newInterpreter!
basePkg := getPluginPackage(tc.pluginType)
builder, err := newYaegiMiddlewareBuilder(interpreter, basePkg, basePkg)
require.NoError(t, err)
require.NotNil(t, builder)
// Verify that unsafe/syscall functions actually work if the plugin uses them
if tc.pluginType != "safe" {
verifyMiddlewareWorks(t, builder)
}
} else {
assert.Error(t, err)
assert.Contains(t, err.Error(), tc.expectedError)
}
})
}
}
// Helper that uses the ACTUAL newInterpreter function with real test plugins
func createInterpreterForTesting(ctx context.Context, goPath, pluginType string, manifestUnsafe, settingsUnsafe bool) (*interp.Interpreter, error) {
pluginImport := getPluginPackage(pluginType)
manifest := &Manifest{
Import: pluginImport,
UseUnsafe: manifestUnsafe,
}
settings := Settings{
UseUnsafe: settingsUnsafe,
}
// Call the ACTUAL production newInterpreter function - no workarounds needed!
return newInterpreter(ctx, goPath, manifest, settings)
}
// Helper to get the correct plugin package name based on type
func getPluginPackage(pluginType string) string {
switch pluginType {
case "safe":
return "testpluginsafe"
case "unsafe-only":
return "testpluginunsafe"
case "unsafe+syscall":
return "testpluginsyscall"
default:
return "testpluginsafe"
}
}
// Helper to verify that unsafe/syscall functions actually work by invoking the middleware
func verifyMiddlewareWorks(t *testing.T, builder *yaegiMiddlewareBuilder) {
t.Helper()
// Create a middleware instance - this will call the plugin's New() function
// which uses unsafe/syscall, proving they work
middleware, err := builder.newMiddleware(map[string]interface{}{
"message": "test",
}, "test-middleware")
require.NoError(t, err, "Should be able to create middleware that uses unsafe/syscall")
require.NotNil(t, middleware, "Middleware should not be nil")
// The fact that we got here without crashing proves unsafe/syscall work!
}

View File

@ -13,7 +13,7 @@ const (
type Settings struct {
Envs []string `description:"Environment variables to forward to the wasm guest." json:"envs,omitempty" toml:"envs,omitempty" yaml:"envs,omitempty"`
Mounts []string `description:"Directory to mount to the wasm guest." json:"mounts,omitempty" toml:"mounts,omitempty" yaml:"mounts,omitempty"`
UseUnsafe bool `description:"Allow the plugin to use unsafe package." json:"useUnsafe,omitempty" toml:"useUnsafe,omitempty" yaml:"useUnsafe,omitempty"`
UseUnsafe bool `description:"Allow the plugin to use unsafe and syscall packages." json:"useUnsafe,omitempty" toml:"useUnsafe,omitempty" yaml:"useUnsafe,omitempty"`
}
// Descriptor The static part of a plugin configuration.