mirror of
https://github.com/wazero/wazero
synced 2026-06-21 14:12:37 +00:00
Always use W^X if possible. (#2429)
This always uses W^X (pages are never both writable and executable at the same time) where possible, which is everywhere except Solaris/illumos because it turns out wrapping `mprotect` there is infeasible (without the helping hand of `x/sys/unix`). This is mostly to improve security (make our executable code harder to corrupt, accidentally or on purpose). I expected it to make performance worse, and tried to mitigate it with #2428). But at least on x64 macOS, it seems to make things better? Not sure if this was a fluke: ```sh % benchstat head wx goos: darwin goarch: amd64 pkg: github.com/tetratelabs/wazero/internal/integration_test/stdlibs cpu: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz │ head │ wx │ │ sec/op │ sec/op vs base │ Zig/Compile/test-opt.wasm-12 5.410 ± ∞ ¹ 4.783 ± ∞ ¹ ~ (p=1.000 n=1) ² Zig/Run/test-opt.wasm-12 34.74 ± ∞ ¹ 33.48 ± ∞ ¹ ~ (p=1.000 n=1) ² Zig/Compile/test.wasm-12 5.978 ± ∞ ¹ 5.412 ± ∞ ¹ ~ (p=1.000 n=1) ² Zig/Run/test.wasm-12 35.67 ± ∞ ¹ 34.47 ± ∞ ¹ ~ (p=1.000 n=1) ² geomean 14.15 13.15 -7.08% ¹ need >= 6 samples for confidence interval at level 0.95 ² need >= 4 samples to detect a difference at alpha level 0.05 │ head │ wx │ │ B/op │ B/op vs base │ Zig/Compile/test-opt.wasm-12 359.5Mi ± ∞ ¹ 359.5Mi ± ∞ ¹ ~ (p=1.000 n=1) ² Zig/Run/test-opt.wasm-12 741.8Mi ± ∞ ¹ 741.8Mi ± ∞ ¹ ~ (p=1.000 n=1) ² Zig/Compile/test.wasm-12 574.3Mi ± ∞ ¹ 574.3Mi ± ∞ ¹ ~ (p=1.000 n=1) ² Zig/Run/test.wasm-12 1.297Gi ± ∞ ¹ 1.297Gi ± ∞ ¹ ~ (p=1.000 n=1) ² geomean 671.5Mi 671.5Mi -0.00% ¹ need >= 6 samples for confidence interval at level 0.95 ² need >= 4 samples to detect a difference at alpha level 0.05 │ head │ wx │ │ allocs/op │ allocs/op vs base │ Zig/Compile/test-opt.wasm-12 336.2k ± ∞ ¹ 336.2k ± ∞ ¹ ~ (p=1.000 n=1) ² Zig/Run/test-opt.wasm-12 52.00k ± ∞ ¹ 52.00k ± ∞ ¹ ~ (p=1.000 n=1) ³ Zig/Compile/test.wasm-12 289.7k ± ∞ ¹ 289.7k ± ∞ ¹ ~ (p=1.000 n=1) ³ Zig/Run/test.wasm-12 2.156M ± ∞ ¹ 2.156M ± ∞ ¹ ~ (p=1.000 n=1) ³ geomean 323.3k 323.3k -0.00% ¹ need >= 6 samples for confidence interval at level 0.95 ² all samples are equal ³ need >= 4 samples to detect a difference at alpha level 0.05 ``` Signed-off-by: Nuno Cruces <ncruces@users.noreply.github.com>
This commit is contained in:
@@ -342,11 +342,8 @@ func (e *engine) compileModule(ctx context.Context, module *wasm.Module, listene
|
||||
machine.ResolveRelocations(refToBinaryOffset, importedFns, executable, rels, callTrampolineIslandOffsets)
|
||||
}
|
||||
|
||||
if runtime.GOARCH == "arm64" {
|
||||
// On arm64, we cannot give all of rwx at the same time, so we change it to exec.
|
||||
if err = platform.MprotectRX(executable); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = platform.MprotectRX(executable); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cm.sharedFunctions = e.sharedFunctions
|
||||
e.setFinalizer(cm.executables, executablesFinalizer)
|
||||
@@ -500,11 +497,8 @@ func (e *engine) compileHostModule(ctx context.Context, module *wasm.Module, lis
|
||||
wazevoapi.PerfMap.Flush(uintptr(unsafe.Pointer(&executable[0])), cm.functionOffsets)
|
||||
}
|
||||
|
||||
if runtime.GOARCH == "arm64" {
|
||||
// On arm64, we cannot give all of rwx at the same time, so we change it to exec.
|
||||
if err = platform.MprotectRX(executable); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = platform.MprotectRX(executable); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e.setFinalizer(cm.executables, executablesFinalizer)
|
||||
return cm, nil
|
||||
@@ -778,11 +772,8 @@ func mmapExecutable(src []byte) []byte {
|
||||
|
||||
copy(executable, src)
|
||||
|
||||
if runtime.GOARCH == "arm64" {
|
||||
// On arm64, we cannot give all of rwx at the same time, so we change it to exec.
|
||||
if err = platform.MprotectRX(executable); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err = platform.MprotectRX(executable); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return executable
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"io"
|
||||
"runtime"
|
||||
"unsafe"
|
||||
|
||||
"github.com/tetratelabs/wazero/experimental"
|
||||
@@ -246,11 +245,8 @@ func deserializeCompiledModule(wazeroVersion string, reader io.ReadCloser) (cm *
|
||||
return nil, false, fmt.Errorf("compilationcache: checksum mismatch (expected %d, got %d)", expected, checksum)
|
||||
}
|
||||
|
||||
if runtime.GOARCH == "arm64" {
|
||||
// On arm64, we cannot give all of rwx at the same time, so we change it to exec.
|
||||
if err = platform.MprotectRX(executable); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if err = platform.MprotectRX(executable); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
cm.executable = executable
|
||||
}
|
||||
|
||||
@@ -59,12 +59,16 @@ func init() {
|
||||
})
|
||||
}
|
||||
|
||||
func mmapCodeSegment(size, prot int) ([]byte, error) {
|
||||
flags := syscall.MAP_ANON | syscall.MAP_PRIVATE
|
||||
func mmapCodeSegment(size int) ([]byte, error) {
|
||||
flag := syscall.MAP_ANON | syscall.MAP_PRIVATE
|
||||
prot := syscall.PROT_READ | syscall.PROT_WRITE
|
||||
if noopMprotectRX {
|
||||
prot = syscall.PROT_READ | syscall.PROT_WRITE | syscall.PROT_EXEC
|
||||
}
|
||||
|
||||
for _, hugePagesConfig := range hugePagesConfigs {
|
||||
if hugePagesConfig.match(size) {
|
||||
b, err := syscall.Mmap(-1, 0, size, prot, flags|hugePagesConfig.flag)
|
||||
b, err := syscall.Mmap(-1, 0, size, prot, flag|hugePagesConfig.flag)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -72,5 +76,5 @@ func mmapCodeSegment(size, prot int) ([]byte, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return syscall.Mmap(-1, 0, size, prot, flags)
|
||||
return syscall.Mmap(-1, 0, size, prot, flag)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,11 @@ package platform
|
||||
|
||||
import "syscall"
|
||||
|
||||
func mmapCodeSegment(size, prot int) ([]byte, error) {
|
||||
func mmapCodeSegment(size int) ([]byte, error) {
|
||||
prot := syscall.PROT_READ | syscall.PROT_WRITE
|
||||
if noopMprotectRX {
|
||||
prot = syscall.PROT_READ | syscall.PROT_WRITE | syscall.PROT_EXEC
|
||||
}
|
||||
return syscall.Mmap(
|
||||
-1,
|
||||
0,
|
||||
|
||||
@@ -2,31 +2,8 @@
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const (
|
||||
mmapProtAMD64 = syscall.PROT_READ | syscall.PROT_WRITE | syscall.PROT_EXEC
|
||||
mmapProtARM64 = syscall.PROT_READ | syscall.PROT_WRITE
|
||||
)
|
||||
import "syscall"
|
||||
|
||||
func munmapCodeSegment(code []byte) error {
|
||||
return syscall.Munmap(code)
|
||||
}
|
||||
|
||||
// mmapCodeSegmentAMD64 gives all read-write-exec permission to the mmap region
|
||||
// to enter the function. Otherwise, segmentation fault exception is raised.
|
||||
func mmapCodeSegmentAMD64(size int) ([]byte, error) {
|
||||
// The region must be RWX: RW for writing native codes, X for executing the region.
|
||||
return mmapCodeSegment(size, mmapProtAMD64)
|
||||
}
|
||||
|
||||
// mmapCodeSegmentARM64 cannot give all read-write-exec permission to the mmap region.
|
||||
// Otherwise, the mmap systemcall would raise an error. Here we give read-write
|
||||
// to the region so that we can write contents at call-sites. Callers are responsible to
|
||||
// execute MprotectRX on the returned buffer.
|
||||
func mmapCodeSegmentARM64(size int) ([]byte, error) {
|
||||
// The region must be RW: RW for writing native codes.
|
||||
return mmapCodeSegment(size, mmapProtARM64)
|
||||
}
|
||||
|
||||
@@ -13,11 +13,7 @@ func munmapCodeSegment(code []byte) error {
|
||||
panic(errUnsupported)
|
||||
}
|
||||
|
||||
func mmapCodeSegmentAMD64(size int) ([]byte, error) {
|
||||
panic(errUnsupported)
|
||||
}
|
||||
|
||||
func mmapCodeSegmentARM64(size int) ([]byte, error) {
|
||||
func mmapCodeSegment(size int) ([]byte, error) {
|
||||
panic(errUnsupported)
|
||||
}
|
||||
|
||||
|
||||
@@ -56,16 +56,7 @@ func virtualProtect(address, size, newprotect uintptr, oldprotect *uint32) error
|
||||
return nil
|
||||
}
|
||||
|
||||
func mmapCodeSegmentAMD64(size int) ([]byte, error) {
|
||||
p, err := allocateMemory(uintptr(size), windows_PAGE_EXECUTE_READWRITE)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return unsafe.Slice((*byte)(unsafe.Pointer(p)), size), nil
|
||||
}
|
||||
|
||||
func mmapCodeSegmentARM64(size int) ([]byte, error) {
|
||||
func mmapCodeSegment(size int) ([]byte, error) {
|
||||
p, err := allocateMemory(uintptr(size), windows_PAGE_READWRITE)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const noopMprotectRX = false
|
||||
|
||||
// MprotectRX is like syscall.Mprotect with RX permission, defined locally so that BSD compiles.
|
||||
func MprotectRX(b []byte) (err error) {
|
||||
var _p0 unsafe.Pointer
|
||||
|
||||
@@ -4,6 +4,8 @@ package platform
|
||||
|
||||
import "syscall"
|
||||
|
||||
const noopMprotectRX = false
|
||||
|
||||
// MprotectRX is like syscall.Mprotect with RX permission.
|
||||
func MprotectRX(b []byte) (err error) {
|
||||
return syscall.Mprotect(b, syscall.PROT_READ|syscall.PROT_EXEC)
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
package platform
|
||||
|
||||
import "syscall"
|
||||
const noopMprotectRX = true
|
||||
|
||||
func MprotectRX(b []byte) error {
|
||||
return syscall.ENOTSUP
|
||||
// Assume we already called mmap with at least RX.
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -40,11 +40,7 @@ func MmapCodeSegment(size int) ([]byte, error) {
|
||||
if size == 0 {
|
||||
panic("BUG: MmapCodeSegment with zero length")
|
||||
}
|
||||
if runtime.GOARCH == "amd64" {
|
||||
return mmapCodeSegmentAMD64(size)
|
||||
} else {
|
||||
return mmapCodeSegmentARM64(size)
|
||||
}
|
||||
return mmapCodeSegment(size)
|
||||
}
|
||||
|
||||
// MunmapCodeSegment unmaps the given memory region.
|
||||
|
||||
Reference in New Issue
Block a user