diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c96811a..ece54b6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -73,10 +73,8 @@ jobs: ENVOY_IMAGE=$image go run mage.go e2e done - # Currently excluded, go-ftw fails to find the destination after a while. Locally works fine. - # - name: Run regression tests (ftw) - # shell: bash - # run: go run mage.go ftw + - name: Run regression tests (ftw) + run: go run mage.go ftw - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 diff --git a/buildtools/mimalloc/Dockerfile b/buildtools/mimalloc/Dockerfile new file mode 100644 index 0000000..5a652c0 --- /dev/null +++ b/buildtools/mimalloc/Dockerfile @@ -0,0 +1,14 @@ +# Copyright The OWASP Coraza contributors +# SPDX-License-Identifier: Apache-2.0 + +FROM ghcr.io/corazawaf/coraza-proxy-wasm/buildtools-wasi-sdk:main + +RUN apt-get install -y cmake patch + +RUN mkdir -p /mimalloc && curl -L https://github.com/microsoft/mimalloc/archive/refs/tags/v2.0.6.tar.gz | tar -xz --strip-components 1 -C /mimalloc +WORKDIR /mimalloc +ADD mimalloc.patch mimalloc.patch +RUN patch -p1 < mimalloc.patch +RUN mkdir -p out/release && cd out/release && cmake ../.. -DMI_BUILD_SHARED=off -DMI_BUILD_TESTS=off && make && llvm-ranlib-14 libmimalloc.a + +CMD ["cp", "./out/release/libmimalloc.a", "/out/libmimalloc.a"] \ No newline at end of file diff --git a/buildtools/mimalloc/mimalloc.patch b/buildtools/mimalloc/mimalloc.patch new file mode 100644 index 0000000..90f5599 --- /dev/null +++ b/buildtools/mimalloc/mimalloc.patch @@ -0,0 +1,34 @@ +diff --git a/include/mimalloc-atomic.h b/include/mimalloc-atomic.h +index 7ad5da5..3029d97 100644 +--- a/include/mimalloc-atomic.h ++++ b/include/mimalloc-atomic.h +@@ -325,7 +325,7 @@ static inline void mi_atomic_yield(void) { + #elif defined(__wasi__) + #include + static inline void mi_atomic_yield(void) { +- sched_yield(); ++ // sched_yield(); + } + #else + #include +diff --git a/src/alloc-override.c b/src/alloc-override.c +index e29cb4b..19f6822 100644 +--- a/src/alloc-override.c ++++ b/src/alloc-override.c +@@ -256,7 +256,7 @@ int reallocarr(void* p, size_t count, size_t size) { return mi_reallocarr(p + void* memalign(size_t alignment, size_t size) { return mi_memalign(alignment, size); } + void* _aligned_malloc(size_t alignment, size_t size) { return mi_aligned_alloc(alignment, size); } + +-#if defined(__GLIBC__) && defined(__linux__) ++#if defined(__GLIBC__) && defined(__linux__) || defined(__wasi__) + // forward __libc interface (needed for glibc-based Linux distributions) + void* __libc_malloc(size_t size) MI_FORWARD1(mi_malloc,size) + void* __libc_calloc(size_t count, size_t size) MI_FORWARD2(mi_calloc,count,size) +@@ -268,6 +268,7 @@ void* _aligned_malloc(size_t alignment, size_t size) { return mi_aligned_allo + void* __libc_pvalloc(size_t size) { return mi_pvalloc(size); } + void* __libc_memalign(size_t alignment, size_t size) { return mi_memalign(alignment,size); } + int __posix_memalign(void** p, size_t alignment, size_t size) { return mi_posix_memalign(p,alignment,size); } ++ void* aligned_alloc(size_t alignment, size_t size) { return mi_aligned_alloc(alignment, size); } + #endif + + #ifdef __cplusplus diff --git a/init_tinygo.go b/init_tinygo.go index 9993a83..98e3ed2 100644 --- a/init_tinygo.go +++ b/init_tinygo.go @@ -5,5 +5,7 @@ package main -// #cgo LDFLAGS: lib/libinjection.a lib/libre2.a lib/libcre2.a lib/libc++.a lib/libc++abi.a lib/libclang_rt.builtins-wasm32.a lib/libaho_corasick.a +import _ "github.com/corazawaf/coraza-proxy-wasm/internal/gc" + +// #cgo LDFLAGS: lib/libinjection.a lib/libre2.a lib/libcre2.a lib/libc++.a lib/libc++abi.a lib/libclang_rt.builtins-wasm32.a lib/libaho_corasick.a lib/libmimalloc.a import "C" diff --git a/internal/calloc/calloc.go b/internal/calloc/calloc.go deleted file mode 100644 index 43d270d..0000000 --- a/internal/calloc/calloc.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright The OWASP Coraza contributors -// SPDX-License-Identifier: Apache-2.0 - -//go:build tinygo - -package calloc - -import "unsafe" - -//export malloc -func libc_malloc(size uintptr) unsafe.Pointer - -//export free -func libc_free(ptr unsafe.Pointer) - -//export calloc -func libc_calloc(nmemb, size uintptr) unsafe.Pointer - -//export __libc_calloc -func __libc_calloc(nmemb, size uintptr) unsafe.Pointer { - return libc_calloc(nmemb, size) -} - -//export __libc_malloc -func __libc_malloc(size uintptr) unsafe.Pointer { - return libc_malloc(size) -} - -//export __libc_free -func __libc_free(ptr unsafe.Pointer) { - libc_free(ptr) -} - -//export posix_memalign -func posix_memalign(memptr *unsafe.Pointer, alignment, size uintptr) int { - // Ignore alignment for now - *memptr = libc_malloc(size) - return 0 -} - -//export aligned_alloc -func aligned_alloc(alignment, size uintptr) unsafe.Pointer { - // Ignore alignment for now - return libc_malloc(size) -} diff --git a/internal/calloc/doc.go b/internal/calloc/doc.go deleted file mode 100644 index 630321c..0000000 --- a/internal/calloc/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright The OWASP Coraza contributors -// SPDX-License-Identifier: Apache-2.0 - -// Package calloc reimplements some memory allocation functions that C++ expects to exist -// but TinyGo does not provide. -package calloc diff --git a/internal/gc/doc.go b/internal/gc/doc.go new file mode 100644 index 0000000..2200f99 --- /dev/null +++ b/internal/gc/doc.go @@ -0,0 +1,9 @@ +// Copyright The OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +// Package gc is a custom gargabe gollector for TinyGo. The main difference is instead of taking +// ownership of the entire process heap, it uses malloc to allocate blocks for the GC to then +// assign to allocated objects. +// +// Currently, only one block can be allocated meaning this has a fixed-size heap. +package gc diff --git a/internal/gc/gc_conservative.go b/internal/gc/gc_conservative.go new file mode 100644 index 0000000..75e5fa8 --- /dev/null +++ b/internal/gc/gc_conservative.go @@ -0,0 +1,590 @@ +// Copyright The OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +//go:build tinygo + +// Copied from https://github.com/tinygo-org/tinygo/blob/3dbc4d52105f4209ece1332f0272f293745ac0bf/src/runtime/gc_conservative.go +// with modifications to use malloc for underlying memory storage. + +package gc + +// This memory manager is a textbook mark/sweep implementation, heavily inspired +// by the MicroPython garbage collector. +// +// The memory manager internally uses blocks of 4 pointers big (see +// bytesPerBlock). Every allocation first rounds up to this size to align every +// block. It will first try to find a chain of blocks that is big enough to +// satisfy the allocation. If it finds one, it marks the first one as the "head" +// and the following ones (if any) as the "tail" (see below). If it cannot find +// any free space, it will perform a garbage collection cycle and try again. If +// it still cannot find any free space, it gives up. +// +// Every block has some metadata, which is stored at the end of the heap. +// The four states are "free", "head", "tail", and "mark". During normal +// operation, there are no marked blocks. Every allocated object starts with a +// "head" and is followed by "tail" blocks. The reason for this distinction is +// that this way, the start and end of every object can be found easily. +// +// Metadata is stored in a special area at the end of the heap, in the area +// metadataStart..heapEnd. The actual blocks are stored in +// heapStart..metadataStart. +// +// More information: +// https://aykevl.nl/2020/09/gc-tinygo +// https://github.com/micropython/micropython/wiki/Memory-Manager +// https://github.com/micropython/micropython/blob/master/py/gc.c +// "The Garbage Collection Handbook" by Richard Jones, Antony Hosking, Eliot +// Moss. + +import ( + "unsafe" +) + +const gcDebug = false + +// disable assertions for the garbage collector +const gcAsserts = false + +// disable assertions for the scheduler +const schedulerAsserts = false + +// Some globals + constants for the entire GC. + +const ( + heapSize = 128 * 1024 * 1024 + wordsPerBlock = 4 // number of pointers in an allocated block + bytesPerBlock = wordsPerBlock * unsafe.Sizeof(heapStart) + stateBits = 2 // how many bits a block state takes (see blockState type) + blocksPerStateByte = 8 / stateBits + markStackSize = 4 * unsafe.Sizeof((*int)(nil)) // number of to-be-marked blocks to queue before forcing a rescan +) + +var ( + heapStart uintptr // start of the heap + heapEnd uintptr // end of the heap (exclusive) + + metadataStart unsafe.Pointer // pointer to the start of the heap metadata + nextAlloc gcBlock // the next block that should be tried by the allocator + endBlock gcBlock // the block just past the end of the available space + gcTotalAlloc uint64 // total number of bytes allocated + gcMallocs uint64 // total number of allocations + gcFrees uint64 // total number of objects freed +) + +// zeroSizedAlloc is just a sentinel that gets returned when allocating 0 bytes. +var zeroSizedAlloc uint8 + +// Provide some abstraction over heap blocks. + +// blockState stores the four states in which a block can be. It is two bits in +// size. +type blockState uint8 + +const ( + blockStateFree blockState = 0 // 00 + blockStateHead blockState = 1 // 01 + blockStateTail blockState = 2 // 10 + blockStateMark blockState = 3 // 11 + blockStateMask blockState = 3 // 11 +) + +// String returns a human-readable version of the block state, for debugging. +func (s blockState) String() string { + switch s { + case blockStateFree: + return "free" + case blockStateHead: + return "head" + case blockStateTail: + return "tail" + case blockStateMark: + return "mark" + default: + // must never happen + return "!err" + } +} + +// The block number in the pool. +type gcBlock uintptr + +// blockFromAddr returns a block given an address somewhere in the heap (which +// might not be heap-aligned). +func blockFromAddr(addr uintptr) gcBlock { + if gcAsserts && (addr < heapStart || addr >= uintptr(metadataStart)) { + panic("gc: trying to get block from invalid address") + } + return gcBlock((addr - heapStart) / bytesPerBlock) +} + +// Return a pointer to the start of the allocated object. +func (b gcBlock) pointer() unsafe.Pointer { + return unsafe.Pointer(b.address()) +} + +// Return the address of the start of the allocated object. +func (b gcBlock) address() uintptr { + addr := heapStart + uintptr(b)*bytesPerBlock + if gcAsserts && addr > uintptr(metadataStart) { + panic("gc: block pointing inside metadata") + } + return addr +} + +// findHead returns the head (first block) of an object, assuming the block +// points to an allocated object. It returns the same block if this block +// already points to the head. +func (b gcBlock) findHead() gcBlock { + for b.state() == blockStateTail { + b-- + } + if gcAsserts { + if b.state() != blockStateHead && b.state() != blockStateMark { + panic("gc: found tail without head") + } + } + return b +} + +// findNext returns the first block just past the end of the tail. This may or +// may not be the head of an object. +func (b gcBlock) findNext() gcBlock { + if b.state() == blockStateHead || b.state() == blockStateMark { + b++ + } + for b.address() < uintptr(metadataStart) && b.state() == blockStateTail { + b++ + } + return b +} + +// State returns the current block state. +func (b gcBlock) state() blockState { + stateBytePtr := (*uint8)(unsafe.Pointer(uintptr(metadataStart) + uintptr(b/blocksPerStateByte))) + return blockState(*stateBytePtr>>((b%blocksPerStateByte)*stateBits)) & blockStateMask +} + +// setState sets the current block to the given state, which must contain more +// bits than the current state. Allowed transitions: from free to any state and +// from head to mark. +func (b gcBlock) setState(newState blockState) { + stateBytePtr := (*uint8)(unsafe.Pointer(uintptr(metadataStart) + uintptr(b/blocksPerStateByte))) + *stateBytePtr |= uint8(newState << ((b % blocksPerStateByte) * stateBits)) + if gcAsserts && b.state() != newState { + panic("gc: setState() was not successful") + } +} + +// markFree sets the block state to free, no matter what state it was in before. +func (b gcBlock) markFree() { + stateBytePtr := (*uint8)(unsafe.Pointer(uintptr(metadataStart) + uintptr(b/blocksPerStateByte))) + *stateBytePtr &^= uint8(blockStateMask << ((b % blocksPerStateByte) * stateBits)) + if gcAsserts && b.state() != blockStateFree { + panic("gc: markFree() was not successful") + } +} + +// unmark changes the state of the block from mark to head. It must be marked +// before calling this function. +func (b gcBlock) unmark() { + if gcAsserts && b.state() != blockStateMark { + panic("gc: unmark() on a block that is not marked") + } + clearMask := blockStateMask ^ blockStateHead // the bits to clear from the state + stateBytePtr := (*uint8)(unsafe.Pointer(uintptr(metadataStart) + uintptr(b/blocksPerStateByte))) + *stateBytePtr &^= uint8(clearMask << ((b % blocksPerStateByte) * stateBits)) + if gcAsserts && b.state() != blockStateHead { + panic("gc: unmark() was not successful") + } +} + +// Initialize the memory allocator. +// No memory may be allocated before this is called. That means the runtime and +// any packages the runtime depends upon may not allocate memory during package +// initialization. +// +//go:linkname initHeap runtime.initHeap +func init() { + heapStart = uintptr(libc_malloc(heapSize)) + heapEnd = heapStart + heapSize + calculateHeapAddresses() + + // Set all block states to 'free'. + metadataSize := heapEnd - uintptr(metadataStart) + memzero(unsafe.Pointer(metadataStart), metadataSize) +} + +// calculateHeapAddresses initializes variables such as metadataStart and +// numBlock based on heapStart and heapEnd. +// +// This function can be called again when the heap size increases. The caller is +// responsible for copying the metadata to the new location. +func calculateHeapAddresses() { + totalSize := heapEnd - heapStart + + // Allocate some memory to keep 2 bits of information about every block. + metadataSize := (totalSize + blocksPerStateByte*bytesPerBlock) / (1 + blocksPerStateByte*bytesPerBlock) + metadataStart = unsafe.Pointer(heapEnd - metadataSize) + + // Use the rest of the available memory as heap. + numBlocks := (uintptr(metadataStart) - heapStart) / bytesPerBlock + endBlock = gcBlock(numBlocks) + if gcDebug { + println("heapStart: ", heapStart) + println("heapEnd: ", heapEnd) + println("total size: ", totalSize) + println("metadata size: ", metadataSize) + println("metadataStart: ", metadataStart) + println("# of blocks: ", numBlocks) + println("# of block states:", metadataSize*blocksPerStateByte) + } + if gcAsserts && metadataSize*blocksPerStateByte < numBlocks { + // sanity check + panic("gc: metadata array is too small") + } +} + +// alloc tries to find some free space on the heap, possibly doing a garbage +// collection cycle if needed. If no space is free, it panics. +// +//go:linkname alloc runtime.alloc +func alloc(size uintptr, layout unsafe.Pointer) unsafe.Pointer { + if size == 0 { + return unsafe.Pointer(&zeroSizedAlloc) + } + + gcTotalAlloc += uint64(size) + gcMallocs++ + + neededBlocks := (size + (bytesPerBlock - 1)) / bytesPerBlock + + // Continue looping until a run of free blocks has been found that fits the + // requested size. + index := nextAlloc + numFreeBlocks := uintptr(0) + heapScanCount := uint8(0) + for { + if index == nextAlloc { + if heapScanCount == 0 { + heapScanCount = 1 + } else if heapScanCount == 1 { + // The entire heap has been searched for free memory, but none + // could be found. Run a garbage collection cycle to reclaim + // free memory and try again. + heapScanCount = 2 + freeBytes := runGC() + heapSize := uintptr(metadataStart) - heapStart + if freeBytes < heapSize/3 { + // Ensure there is at least 33% headroom. + // This percentage was arbitrarily chosen, and may need to + // be tuned in the future. + growHeap() + } + } else { + // Even after garbage collection, no free memory could be found. + // Try to increase heap size. + if growHeap() { + // Success, the heap was increased in size. Try again with a + // larger heap. + } else { + // Unfortunately the heap could not be increased. This + // happens on baremetal systems for example (where all + // available RAM has already been dedicated to the heap). + panic("out of memory") + } + } + } + + // Wrap around the end of the heap. + if index == endBlock { + index = 0 + // Reset numFreeBlocks as allocations cannot wrap. + numFreeBlocks = 0 + // In rare cases, the initial heap might be so small that there are + // no blocks at all. In this case, it's better to jump back to the + // start of the loop and try again, until the GC realizes there is + // no memory and grows the heap. + // This can sometimes happen on WebAssembly, where the initial heap + // is created by whatever is left on the last memory page. + continue + } + + // Is the block we're looking at free? + if index.state() != blockStateFree { + // This block is in use. Try again from this point. + numFreeBlocks = 0 + index++ + continue + } + numFreeBlocks++ + index++ + + // Are we finished? + if numFreeBlocks == neededBlocks { + // Found a big enough range of free blocks! + nextAlloc = index + thisAlloc := index - gcBlock(neededBlocks) + if gcDebug { + println("found memory:", thisAlloc.pointer(), int(size)) + } + + // Set the following blocks as being allocated. + thisAlloc.setState(blockStateHead) + for i := thisAlloc + 1; i != nextAlloc; i++ { + i.setState(blockStateTail) + } + + // Return a pointer to this allocation. + pointer := thisAlloc.pointer() + memzero(pointer, size) + return pointer + } + } +} + +// GC performs a garbage collection cycle. +func GC() { + runGC() +} + +// runGC performs a garbage colleciton cycle. It is the internal implementation +// of the runtime.GC() function. The difference is that it returns the number of +// free bytes in the heap after the GC is finished. +func runGC() (freeBytes uintptr) { + if gcDebug { + println("running collection cycle...") + } + + // Mark phase: mark all reachable objects, recursively. + markStack() + markGlobals() + finishMark() + + // Sweep phase: free all non-marked objects and unmark marked objects for + // the next collection cycle. + freeBytes = sweep() + + // Show how much has been sweeped, for debugging. + if gcDebug { + dumpHeap() + } + + return +} + +// markRoots reads all pointers from start to end (exclusive) and if they look +// like a heap pointer and are unmarked, marks them and scans that object as +// well (recursively). The start and end parameters must be valid pointers and +// must be aligned. +func markRoots(start, end uintptr) { + if gcDebug { + println("mark from", start, "to", end, int(end-start)) + } + if gcAsserts { + if start >= end { + panic("gc: unexpected range to mark") + } + if start%unsafe.Alignof(start) != 0 { + panic("gc: unaligned start pointer") + } + if end%unsafe.Alignof(end) != 0 { + panic("gc: unaligned end pointer") + } + } + + // Reduce the end bound to avoid reading too far on platforms where pointer alignment is smaller than pointer size. + // If the size of the range is 0, then end will be slightly below start after this. + end -= unsafe.Sizeof(end) - unsafe.Alignof(end) + + for addr := start; addr < end; addr += unsafe.Alignof(addr) { + root := *(*uintptr)(unsafe.Pointer(addr)) + markRoot(addr, root) + } +} + +// stackOverflow is a flag which is set when the GC scans too deep while marking. +// After it is set, all marked allocations must be re-scanned. +var stackOverflow bool + +// startMark starts the marking process on a root and all of its children. +func startMark(root gcBlock) { + var stack [markStackSize]gcBlock + stack[0] = root + root.setState(blockStateMark) + stackLen := 1 + for stackLen > 0 { + // Pop a block off of the stack. + stackLen-- + block := stack[stackLen] + if gcDebug { + println("stack popped, remaining stack:", stackLen) + } + + // Scan all pointers inside the block. + start, end := block.address(), block.findNext().address() + for addr := start; addr != end; addr += unsafe.Alignof(addr) { + // Load the word. + word := *(*uintptr)(unsafe.Pointer(addr)) + + if !looksLikePointer(word) { + // Not a heap pointer. + continue + } + + // Find the corresponding memory block. + referencedBlock := blockFromAddr(word) + + if referencedBlock.state() == blockStateFree { + // The to-be-marked object doesn't actually exist. + // This is probably a false positive. + if gcDebug { + println("found reference to free memory:", word, "at:", addr) + } + continue + } + + // Move to the block's head. + referencedBlock = referencedBlock.findHead() + + if referencedBlock.state() == blockStateMark { + // The block has already been marked by something else. + continue + } + + // Mark block. + if gcDebug { + println("marking block:", referencedBlock) + } + referencedBlock.setState(blockStateMark) + + if stackLen == len(stack) { + // The stack is full. + // It is necessary to rescan all marked blocks once we are done. + stackOverflow = true + if gcDebug { + println("gc stack overflowed") + } + continue + } + + // Push the pointer onto the stack to be scanned later. + stack[stackLen] = referencedBlock + stackLen++ + } + } +} + +// finishMark finishes the marking process by processing all stack overflows. +func finishMark() { + for stackOverflow { + // Re-mark all blocks. + stackOverflow = false + for block := gcBlock(0); block < endBlock; block++ { + if block.state() != blockStateMark { + // Block is not marked, so we do not need to rescan it. + continue + } + + // Re-mark the block. + startMark(block) + } + } +} + +// mark a GC root at the address addr. +func markRoot(addr, root uintptr) { + if looksLikePointer(root) { + block := blockFromAddr(root) + if block.state() == blockStateFree { + // The to-be-marked object doesn't actually exist. + // This could either be a dangling pointer (oops!) but most likely + // just a false positive. + return + } + head := block.findHead() + if head.state() != blockStateMark { + if gcDebug { + println("found unmarked pointer", root, "at address", addr) + } + startMark(head) + } + } +} + +// Sweep goes through all memory and frees unmarked memory. +// It returns how many bytes are free in the heap after the sweep. +func sweep() (freeBytes uintptr) { + freeCurrentObject := false + for block := gcBlock(0); block < endBlock; block++ { + switch block.state() { + case blockStateHead: + // Unmarked head. Free it, including all tail blocks following it. + block.markFree() + freeCurrentObject = true + gcFrees++ + freeBytes += bytesPerBlock + case blockStateTail: + if freeCurrentObject { + // This is a tail object following an unmarked head. + // Free it now. + block.markFree() + freeBytes += bytesPerBlock + } + case blockStateMark: + // This is a marked object. The next tail blocks must not be freed, + // but the mark bit must be removed so the next GC cycle will + // collect this object if it is unreferenced then. + block.unmark() + freeCurrentObject = false + case blockStateFree: + freeBytes += bytesPerBlock + } + } + return +} + +// looksLikePointer returns whether this could be a pointer. Currently, it +// simply returns whether it lies anywhere in the heap. Go allows interior +// pointers so we can't check alignment or anything like that. +func looksLikePointer(ptr uintptr) bool { + return ptr >= heapStart && ptr < uintptr(metadataStart) +} + +// dumpHeap can be used for debugging purposes. It dumps the state of each heap +// block to standard output. +func dumpHeap() { + println("heap:") + for block := gcBlock(0); block < endBlock; block++ { + switch block.state() { + case blockStateHead: + print("*") + case blockStateTail: + print("-") + case blockStateMark: + print("#") + default: // free + print("ยท") + } + if block%64 == 63 || block+1 == endBlock { + println() + } + } +} + +func KeepAlive(x interface{}) { + // Unimplemented. Only required with SetFinalizer(). +} + +func SetFinalizer(obj interface{}, finalizer interface{}) { + // Unimplemented. +} + +//export malloc +func libc_malloc(size uintptr) unsafe.Pointer + +//export free +func libc_free(ptr unsafe.Pointer) + +func growHeap() bool { + return false +} diff --git a/internal/gc/gc_globals.go b/internal/gc/gc_globals.go new file mode 100644 index 0000000..85e6cc1 --- /dev/null +++ b/internal/gc/gc_globals.go @@ -0,0 +1,33 @@ +// Copyright The OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +//go:build tinygo + +// Copied from https://github.com/tinygo-org/tinygo/blob/3dbc4d52105f4209ece1332f0272f293745ac0bf/src/runtime/gc_globals.go +// with private start symbols redefined. + +package gc + +import "unsafe" + +//go:extern __heap_base +var heapStartSymbol [0]byte + +//go:extern __global_base +var globalsStartSymbol [0]byte + +var ( + globalsStart = uintptr(unsafe.Pointer(&globalsStartSymbol)) + globalsEnd = uintptr(unsafe.Pointer(&heapStartSymbol)) +) + +// This file implements markGlobals for all the files that don't have a more +// specific implementation. + +// markGlobals marks all globals, which are reachable by definition. +// +// This implementation marks all globals conservatively and assumes it can use +// linker-defined symbols for the start and end of the .data section. +func markGlobals() { + markRoots(globalsStart, globalsEnd) +} diff --git a/internal/gc/gc_stack_portable.go b/internal/gc/gc_stack_portable.go new file mode 100644 index 0000000..6d02dee --- /dev/null +++ b/internal/gc/gc_stack_portable.go @@ -0,0 +1,54 @@ +// Copyright The OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +//go:build tinygo + +// Copied from https://github.com/tinygo-org/tinygo/blob/3dbc4d52105f4209ece1332f0272f293745ac0bf/src/runtime/gc_stack_portable.go +// with go:linkname used to override functions in the runtime package. + +package gc + +import ( + "unsafe" +) + +//go:linkname stackChainStart runtime.stackChainStart +var stackChainStart *stackChainObject + +type stackChainObject struct { + parent *stackChainObject + numSlots uintptr +} + +// markStack marks all root pointers found on the stack. +// +// This implementation is conservative and relies on the compiler inserting code +// to manually push/pop stack objects that are stored in a linked list starting +// with stackChainStart. Manually keeping track of stack values is _much_ more +// expensive than letting the compiler do it and it inhibits a few important +// optimizations, but it has the big advantage of being portable to basically +// any ISA, including WebAssembly. +func markStack() { + stackObject := stackChainStart + for stackObject != nil { + start := uintptr(unsafe.Pointer(stackObject)) + unsafe.Sizeof(uintptr(0))*2 + end := start + stackObject.numSlots*unsafe.Alignof(uintptr(0)) + markRoots(start, end) + stackObject = stackObject.parent + } +} + +// trackPointer is a stub function call inserted by the compiler during IR +// construction. Calls to it are later replaced with regular stack bookkeeping +// code. +// +//go:linkname trackPointer runtime.trackPointer +func trackPointer(ptr unsafe.Pointer) + +// swapStackChain swaps the stack chain. +// This is called from internal/task when switching goroutines. +// +//go:linkname swapStackChain runtime.swapStackChain +func swapStackChain(dst **stackChainObject) { + *dst, stackChainStart = stackChainStart, *dst +} diff --git a/internal/gc/mem.go b/internal/gc/mem.go new file mode 100644 index 0000000..8ebb86e --- /dev/null +++ b/internal/gc/mem.go @@ -0,0 +1,14 @@ +// Copyright The OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +//go:build tinygo + +package gc + +import "unsafe" + +//go:linkname memzero runtime.memzero +func memzero(ptr unsafe.Pointer, size uintptr) + +//go:linkname memcpy runtime.memcpy +func memcpy(dst, src unsafe.Pointer, size uintptr) diff --git a/lib/libmimalloc.a b/lib/libmimalloc.a new file mode 100644 index 0000000..6cd0193 Binary files /dev/null and b/lib/libmimalloc.a differ diff --git a/magefile.go b/magefile.go index c393a43..15488a6 100644 --- a/magefile.go +++ b/magefile.go @@ -168,7 +168,7 @@ func Build() error { return err } - var buildTags []string + buildTags := []string{"custommalloc"} if os.Getenv("TIMING") == "true" { buildTags = append(buildTags, "timing", "proxywasm_timing") } @@ -176,10 +176,7 @@ func Build() error { buildTags = append(buildTags, "memstats") } - buildTagArg := "" - if len(buildTags) > 0 { - buildTagArg = fmt.Sprintf("-tags='%s'", strings.Join(buildTags, " ")) - } + buildTagArg := fmt.Sprintf("-tags='%s'", strings.Join(buildTags, " ")) // ~100MB initial heap initialPages := 2100 @@ -191,7 +188,16 @@ func Build() error { } } - if err := sh.RunV("tinygo", "build", "-opt=2", "-o", filepath.Join("build", "mainraw.wasm"), "-scheduler=none", "-target=wasi", buildTagArg); err != nil { + wd, err := os.Getwd() + if err != nil { + return err + } + + script := fmt.Sprintf(` +cd /src && \ +tinygo build -gc=none -opt=2 -o %s -scheduler=none -target=wasi %s`, filepath.Join("build", "mainraw.wasm"), buildTagArg) + if err := sh.RunV("docker", "run", "--pull=always", "--rm", "-v", fmt.Sprintf("%s:/src", wd), "ghcr.io/corazawaf/coraza-proxy-wasm/buildtools-tinygo:main", + "bash", "-c", script); err != nil { return err } diff --git a/main.go b/main.go index 095c621..1c3caaf 100644 --- a/main.go +++ b/main.go @@ -14,7 +14,6 @@ import ( "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm" "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types" - _ "github.com/corazawaf/coraza-proxy-wasm/internal/calloc" "github.com/corazawaf/coraza-proxy-wasm/internal/operators" )