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.