diff --git a/docs/content/reference/install-configuration/configuration-options.md b/docs/content/reference/install-configuration/configuration-options.md index 3bd7e708e..3da346ee6 100644 --- a/docs/content/reference/install-configuration/configuration-options.md +++ b/docs/content/reference/install-configuration/configuration-options.md @@ -131,14 +131,14 @@ THIS FILE MUST NOT BE EDITED BY HAND | experimental.localplugins._name_.settings | Plugin's settings (works only for wasm plugins). | | | experimental.localplugins._name_.settings.envs | Environment variables to forward to the wasm guest. | | | experimental.localplugins._name_.settings.mounts | Directory to mount to the wasm guest. | | -| experimental.localplugins._name_.settings.useunsafe | Allow the plugin to use unsafe package. | false | +| experimental.localplugins._name_.settings.useunsafe | Allow the plugin to use unsafe and syscall packages. | false | | experimental.otlplogs | Enables the OpenTelemetry logs integration. | false | | experimental.plugins._name_.hash | plugin's hash to validate' | | | experimental.plugins._name_.modulename | plugin's module name. | | | experimental.plugins._name_.settings | Plugin's settings (works only for wasm plugins). | | | experimental.plugins._name_.settings.envs | Environment variables to forward to the wasm guest. | | | experimental.plugins._name_.settings.mounts | Directory to mount to the wasm guest. | | -| experimental.plugins._name_.settings.useunsafe | Allow the plugin to use unsafe package. | false | +| experimental.plugins._name_.settings.useunsafe | Allow the plugin to use unsafe and syscall packages. | false | | experimental.plugins._name_.version | plugin's version. | | | global.checknewversion | Periodically check if a new version has been released. | true | | global.sendanonymoususage | Periodically send anonymous usage statistics. If the option is not specified, it will be disabled by default. | false | diff --git a/docs/content/reference/static-configuration/cli-ref.md b/docs/content/reference/static-configuration/cli-ref.md index 6c9a2657a..ec0d3cc58 100644 --- a/docs/content/reference/static-configuration/cli-ref.md +++ b/docs/content/reference/static-configuration/cli-ref.md @@ -367,7 +367,7 @@ Environment variables to forward to the wasm guest. Directory to mount to the wasm guest. `--experimental.localplugins..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..settings.useunsafe`: -Allow the plugin to use unsafe package. (Default: ```false```) +Allow the plugin to use unsafe and syscall packages. (Default: ```false```) `--experimental.plugins..version`: plugin's version. diff --git a/docs/content/reference/static-configuration/env-ref.md b/docs/content/reference/static-configuration/env-ref.md index fdf963498..0148de541 100644 --- a/docs/content/reference/static-configuration/env-ref.md +++ b/docs/content/reference/static-configuration/env-ref.md @@ -367,7 +367,7 @@ Environment variables to forward to the wasm guest. Directory to mount to the wasm guest. `TRAEFIK_EXPERIMENTAL_LOCALPLUGINS__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__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__VERSION`: plugin's version. diff --git a/pkg/plugins/fixtures/src/testpluginsafe/go.mod b/pkg/plugins/fixtures/src/testpluginsafe/go.mod new file mode 100644 index 000000000..ffc61e1ba --- /dev/null +++ b/pkg/plugins/fixtures/src/testpluginsafe/go.mod @@ -0,0 +1,3 @@ +module testpluginsafe + +go 1.23.0 diff --git a/pkg/plugins/fixtures/src/testpluginsafe/testpluginsafe.go b/pkg/plugins/fixtures/src/testpluginsafe/testpluginsafe.go new file mode 100644 index 000000000..d9ca33cde --- /dev/null +++ b/pkg/plugins/fixtures/src/testpluginsafe/testpluginsafe.go @@ -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 +} diff --git a/pkg/plugins/fixtures/src/testpluginsyscall/go.mod b/pkg/plugins/fixtures/src/testpluginsyscall/go.mod new file mode 100644 index 000000000..871d361b1 --- /dev/null +++ b/pkg/plugins/fixtures/src/testpluginsyscall/go.mod @@ -0,0 +1,3 @@ +module testpluginsyscall + +go 1.23.0 \ No newline at end of file diff --git a/pkg/plugins/fixtures/src/testpluginsyscall/testpluginsyscall.go b/pkg/plugins/fixtures/src/testpluginsyscall/testpluginsyscall.go new file mode 100644 index 000000000..41272fb27 --- /dev/null +++ b/pkg/plugins/fixtures/src/testpluginsyscall/testpluginsyscall.go @@ -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 +} diff --git a/pkg/plugins/fixtures/src/testpluginunsafe/go.mod b/pkg/plugins/fixtures/src/testpluginunsafe/go.mod new file mode 100644 index 000000000..ef0bee089 --- /dev/null +++ b/pkg/plugins/fixtures/src/testpluginunsafe/go.mod @@ -0,0 +1,3 @@ +module testpluginunsafe + +go 1.23.0 \ No newline at end of file diff --git a/pkg/plugins/fixtures/src/testpluginunsafe/testpluginunsafe.go b/pkg/plugins/fixtures/src/testpluginunsafe/testpluginunsafe.go new file mode 100644 index 000000000..1a58901dd --- /dev/null +++ b/pkg/plugins/fixtures/src/testpluginunsafe/testpluginunsafe.go @@ -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 +} diff --git a/pkg/plugins/middlewareyaegi.go b/pkg/plugins/middlewareyaegi.go index 590938044..41d120158 100644 --- a/pkg/plugins/middlewareyaegi.go +++ b/pkg/plugins/middlewareyaegi.go @@ -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()) diff --git a/pkg/plugins/middlewareyaegi_test.go b/pkg/plugins/middlewareyaegi_test.go new file mode 100644 index 000000000..f9df8232d --- /dev/null +++ b/pkg/plugins/middlewareyaegi_test.go @@ -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! +} diff --git a/pkg/plugins/types.go b/pkg/plugins/types.go index 75bb589b3..aa05cc320 100644 --- a/pkg/plugins/types.go +++ b/pkg/plugins/types.go @@ -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.