Repository: tetratelabs/wazero
Pull request #1864 (2023-12-12 18:56:21)

fuzz: ensures the alternate stack for signal handling 
This took me at least 3 months since the first encounter to figure out what's going on.



Patch:
diff --git a/Makefile b/Makefile
index 7e6d7aaf..279a45b4 100644
--- a/Makefile
+++ b/Makefile
@@ -258,6 +258,7 @@ clean: ## Ensure a clean build
 fuzz_timeout_seconds ?= 10
 .PHONY: fuzz
 fuzz:
+	@cd internal/integration_test/fuzz && cargo test
 	@cd internal/integration_test/fuzz && cargo fuzz run no_diff --sanitizer=none -- -rss_limit_mb=8192 -max_total_time=$(fuzz_timeout_seconds)
 	@cd internal/integration_test/fuzz && cargo fuzz run memory_no_diff --sanitizer=none -- -rss_limit_mb=8192 -max_total_time=$(fuzz_timeout_seconds)
 	@cd internal/integration_test/fuzz && cargo fuzz run validation --sanitizer=none -- -rss_limit_mb=8192 -max_total_time=$(fuzz_timeout_seconds)
diff --git a/internal/sysfs/osfile.go b/internal/sysfs/osfile.go
index cecab8cb..952a0ca9 100644
--- a/internal/sysfs/osfile.go
+++ b/internal/sysfs/osfile.go
@@ -98,9 +98,38 @@ func (f *osFile) reopen() (errno experimentalsys.Errno) {
 	// Clear any create flag, as we are re-opening, not re-creating.
 	f.flag &= ^experimentalsys.O_CREAT
 
+	var (
+		isDir  bool
+		offset int64
+		err    error
+	)
+
+	isDir, errno = f.IsDir()
+	if errno != 0 {
+		return errno
+	}
+
+	if !isDir {
+		offset, err = f.file.Seek(0, 1)
+		if err != nil {
+			return experimentalsys.UnwrapOSError(err)
+		}
+	}
+
 	_ = f.close()
 	f.file, errno = OpenFile(f.path, f.flag, f.perm)
-	return
+	if errno != 0 {
+		return errno
+	}
+
+	if !isDir {
+		_, err = f.file.Seek(offset, 0)
+		if err != nil {
+			return experimentalsys.UnwrapOSError(err)
+		}
+	}
+
+	return 0
 }
 
 // IsNonblock implements the same method as documented on fsapi.File


Test patch:
diff --git a/imports/wasi_snapshot_preview1/fs_test.go b/imports/wasi_snapshot_preview1/fs_test.go
index c17c6d9d..6cae4d0d 100644
--- a/imports/wasi_snapshot_preview1/fs_test.go
+++ b/imports/wasi_snapshot_preview1/fs_test.go
@@ -597,6 +597,13 @@ func Test_fdFdstatSetFlags(t *testing.T) {
 `, "\n"+log.String()) // FIXME? flags==0 prints 'flags='
 	log.Reset()
 
+	requireErrnoResult(t, wasip1.ErrnoSuccess, mod, wasip1.FdSeekName, uint64(fd), uint64(0), uint64(0), uint64(1024))
+	require.Equal(t, `
+==> wasi_snapshot_preview1.fd_seek(fd=4,offset=0,whence=0)
+<== (newoffset=0,errno=ESUCCESS)
+`, "\n"+log.String())
+	log.Reset()
+
 	// Without O_APPEND flag, the data is written at the beginning.
 	writeWazero()
 	requireFileContent("wazero6789" + "wazero")
@@ -609,6 +616,16 @@ func Test_fdFdstatSetFlags(t *testing.T) {
 `, "\n"+log.String()) // FIXME? flags==1 prints 'flags=APPEND'
 	log.Reset()
 
+	// Restoring the O_APPEND flag should not reset fd offset.
+	requireErrnoResult(t, wasip1.ErrnoSuccess, mod, wasip1.FdTellName, uint64(fd), uint64(1024))
+	require.Equal(t, `
+==> wasi_snapshot_preview1.fd_tell(fd=4,result.offset=1024)
+<== errno=ESUCCESS
+`, "\n"+log.String())
+	log.Reset()
+	offset, _ := mod.Memory().Read(1024, 4)
+	require.Equal(t, offset, []byte{6, 0, 0, 0})
+
 	// with O_APPEND flag, the data is appended to buffer.
 	writeWazero()
 	requireFileContent("wazero6789" + "wazero" + "wazero")
diff --git a/internal/integration_test/fuzz/Cargo.lock b/internal/integration_test/fuzz/Cargo.lock
index cf3e4e42..20f3fe14 100644
--- a/internal/integration_test/fuzz/Cargo.lock
+++ b/internal/integration_test/fuzz/Cargo.lock
@@ -2,6 +2,18 @@
 # It is not intended for manual editing.
 version = 3
 
+[[package]]
+name = "ahash"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
 [[package]]
 name = "anyhow"
 version = "1.0.58"
@@ -23,6 +35,12 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
 
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
 [[package]]
 name = "cc"
 version = "1.0.73"
@@ -32,6 +50,22 @@ dependencies = [
  "jobserver",
 ]
 
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "ctor"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30d2b3721e861707777e3195b0158f950ae6dc4a27e4d02ff9f67e3eb3de199e"
+dependencies = [
+ "quote",
+ "syn 2.0.40",
+]
+
 [[package]]
 name = "derive_arbitrary"
 version = "1.1.3"
@@ -40,7 +74,7 @@ checksum = "c9a577516173adb681466d517d39bd468293bc2c2a16439375ef0f35bba45f3d"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 1.0.98",
 ]
 
 [[package]]
@@ -63,9 +97,12 @@ checksum = "607c8a29735385251a339424dd462993c0fed8fa09d378f259377df08c126022"
 
 [[package]]
 name = "hashbrown"
-version = "0.14.0"
+version = "0.14.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
+checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
+dependencies = [
+ "ahash",
+]
 
 [[package]]
 name = "indexmap"
@@ -84,7 +121,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
 dependencies = [
  "equivalent",
- "hashbrown 0.14.0",
+ "hashbrown 0.14.3",
 ]
 
 [[package]]
@@ -119,35 +156,57 @@ dependencies = [
  "once_cell",
 ]
 
+[[package]]
+name = "memoffset"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "nix"
+version = "0.23.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c"
+dependencies = [
+ "bitflags",
+ "cc",
+ "cfg-if",
+ "libc",
+ "memoffset",
+]
+
 [[package]]
 name = "once_cell"
-version = "1.13.0"
+version = "1.19.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.40"
+version = "1.0.70"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7"
+checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b"
 dependencies = [
  "unicode-ident",
 ]
 
 [[package]]
 name = "quote"
-version = "1.0.20"
+version = "1.0.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804"
+checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
 dependencies = [
  "proc-macro2",
 ]
 
 [[package]]
 name = "semver"
-version = "1.0.18"
+version = "1.0.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
+checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090"
 
 [[package]]
 name = "syn"
@@ -160,33 +219,50 @@ dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "syn"
+version = "2.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13fa70a4ee923979ffb522cacce59d34421ebdea5625e1073c4326ef9d2dd42e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
 [[package]]
 name = "unicode-ident"
 version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c"
 
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
 [[package]]
 name = "wasm-encoder"
-version = "0.34.1"
+version = "0.37.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f14a94e06a3e2ed1af4e80cac712fed883142019ebe33c3899fd1b5e8550df9d"
+checksum = "7d135e8940b69dbee0f5b0a0be9c1cd6fa8b71d774904c13a3fcfc5dc265e43d"
 dependencies = [
  "leb128",
 ]
 
 [[package]]
 name = "wasm-smith"
-version = "0.12.20"
+version = "0.12.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c368a04ec656f45cf36c860dca97654d94679b260f1f1f3ef6a5bb1e43ad4fd"
+checksum = "4d9a642a2aa8a998228a247036d0f34470a07afc146231bd5c22cc61b8b51e73"
 dependencies = [
  "arbitrary",
  "flagset",
  "indexmap 2.0.0",
  "leb128",
  "wasm-encoder",
- "wasmparser 0.114.0",
+ "wasmparser 0.117.0",
 ]
 
 [[package]]
@@ -200,10 +276,11 @@ dependencies = [
 
 [[package]]
 name = "wasmparser"
-version = "0.114.0"
+version = "0.117.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ef211410dcb08b037eb6d197b2398f8ef9d635c5dc5598d0dfda32094315ea3"
+checksum = "9b206de0c992af9f0b51ef2fb9455623e0a19eb68f172cd8ba9cd0e46637f5ab"
 dependencies = [
+ "hashbrown 0.14.3",
  "indexmap 2.0.0",
  "semver",
 ]
@@ -222,7 +299,30 @@ dependencies = [
 name = "wazero-fuzz-fuzz"
 version = "0.0.0"
 dependencies = [
+ "ctor",
+ "libc",
  "libfuzzer-sys",
+ "nix",
  "wasm-smith",
  "wasmprinter",
 ]
+
+[[package]]
+name = "zerocopy"
+version = "0.7.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "306dca4455518f1f31635ec308b6b3e4eb1b11758cefafc782827d0aa7acb5c7"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.7.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be912bf68235a88fbefd1b73415cb218405958d1655b2ece9035a19920bdf6ba"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.40",
+]
diff --git a/internal/integration_test/fuzz/fuzz/Cargo.toml b/internal/integration_test/fuzz/fuzz/Cargo.toml
index 06bc5a4c..412d7524 100644
--- a/internal/integration_test/fuzz/fuzz/Cargo.toml
+++ b/internal/integration_test/fuzz/fuzz/Cargo.toml
@@ -12,6 +12,9 @@ cargo-fuzz = true
 libfuzzer-sys = "0.4.7"
 wasm-smith = "0.12.20"
 wasmprinter = "0.2.39"
+libc = "0.2"
+nix = "0.23.0"
+ctor = "0.2.6"
 
 [[bin]]
 name = "memory_no_diff"
diff --git a/internal/integration_test/fuzz/fuzz/fuzz_targets/wazero_abi.rs b/internal/integration_test/fuzz/fuzz/fuzz_targets/wazero_abi.rs
index 8314f79a..09702831 100644
--- a/internal/integration_test/fuzz/fuzz/fuzz_targets/wazero_abi.rs
+++ b/internal/integration_test/fuzz/fuzz/fuzz_targets/wazero_abi.rs
@@ -15,3 +15,69 @@ extern "C" {
     #[allow(dead_code)]
     pub fn validate(binary_ptr: *const u8, binary_size: usize);
 }
+
+use ctor::ctor;
+use libc::SIGSTKSZ;
+use nix::libc::{sigaltstack, stack_t};
+use nix::sys::signal::{sigaction, SaFlags, SigAction, SigHandler, SigSet, Signal};
+use std::ptr::null_mut;
+
+#[ctor]
+/// Sets up the separate stack for signal handlers, and sets the SA_ONSTACK flag for signals that are handled by libFuzzer
+///  https://github.com/llvm/llvm-project/blob/8eff5704829ba5edd28754fd9ec7665b34fde22a/compiler-rt/lib/fuzzer/FuzzerUtilPosix.cpp#L117-L141
+/// in order to ensure that Go's stacks won't get corrupted accidentally.
+///
+/// This is necessary due to the undocumented requirement/behavior of Go runtime, and for detail,
+/// see the detailed comments in `tests/sigstack.rs`.
+fn setup_sig_handlers() {
+    set_signal_stack();
+    set_sa_on_stack(Signal::SIGABRT);
+    set_sa_on_stack(Signal::SIGALRM);
+    set_sa_on_stack(Signal::SIGBUS);
+    set_sa_on_stack(Signal::SIGFPE);
+    set_sa_on_stack(Signal::SIGILL);
+    set_sa_on_stack(Signal::SIGINT);
+    set_sa_on_stack(Signal::SIGSEGV);
+    set_sa_on_stack(Signal::SIGTERM);
+    set_sa_on_stack(Signal::SIGXFSZ);
+    set_sa_on_stack(Signal::SIGUSR1);
+    set_sa_on_stack(Signal::SIGUSR2);
+}
+
+/// Sets the SA_ONSTACK flag for the given signal.
+fn set_sa_on_stack(sig: Signal) {
+    let old_action = unsafe {
+        let tmp = SigAction::new(SigHandler::SigDfl, SaFlags::empty(), SigSet::empty());
+        sigaction(sig, &tmp).unwrap()
+    };
+    // Create a new SigAction with the SA_ONSTACK flag added.
+    let new_flags = old_action.flags() | SaFlags::SA_ONSTACK;
+    let new_action = SigAction::new(old_action.handler(), new_flags, old_action.mask());
+    unsafe {
+        sigaction(sig, &new_action).unwrap();
+    }
+}
+
+/// Sets up the separate stack for signal handlers.
+fn set_signal_stack() {
+    // Allocate a new stack for signal handlers to run on.
+    const STACK_SIZE: usize = SIGSTKSZ * 2;
+    let mut stack = vec![0u8; STACK_SIZE];
+
+    let stack_ptr = stack.as_mut_ptr();
+
+    let signal_stack = stack_t {
+        ss_sp: stack_ptr as *mut libc::c_void,
+        ss_flags: 0,
+        ss_size: STACK_SIZE,
+    };
+
+    unsafe {
+        if sigaltstack(&signal_stack, null_mut()) != 0 {
+            panic!("Failed to set alternate signal stack");
+        }
+
+        // Leak the stack vector to prevent it from being dropped.
+        std::mem::forget(stack);
+    }
+}
diff --git a/internal/integration_test/fuzz/fuzz/tests/sigstack.rs b/internal/integration_test/fuzz/fuzz/tests/sigstack.rs
new file mode 100644
index 00000000..f85e23de
--- /dev/null
+++ b/internal/integration_test/fuzz/fuzz/tests/sigstack.rs
@@ -0,0 +1,69 @@
+extern crate libc;
+extern crate nix;
+
+use libc::SIGSTKSZ;
+use libc::{pthread_kill, pthread_self, SIGUSR1};
+use nix::sys::signal;
+use nix::sys::signal::{sigaction, SaFlags, SigAction, SigHandler, SigSet};
+use std::thread;
+use std::time::Duration;
+
+const STACK_SIZE: usize = SIGSTKSZ * 4;
+
+#[test]
+fn main() {
+    unsafe {
+        let sa = SigAction::new(
+            SigHandler::SigAction(handler),
+            // Set SA_ONSTACK to ensure the signal handler runs on the alternate stack.
+            // The alternate stack is prepared by the Go runtime if there's not the one by the host C program
+            // via the sigaltstack syscall. However if this flag is not set, which happens when
+            // the host C program not having intention to deal with signals gracefully (e.g. stack overflow),
+            // the signal handler (installed by either C program or Go runtime) will run on
+            // the "current stack". That is problematic when a signal handling happens during execution
+            // of wazevo function because it uses "Go allocated" stack.
+            //
+            // On the other hand, this is more of a general problem for any C program that uses Go as a library,
+            // not limited to wazevo, when it does not not install sig handlers with SA_ONSTACK. That means
+            // any Gorountime stack could result in being used during signal handling, which can potentially
+            // cause any memory corruption. I would say such C program is using Go library in a dangerous way.
+            //
+            // To reproduce the failure in wazevo, Use SaFlags::empty() and wazevoapi.StackGuardCheckEnabled=true.
+            //
+            // Note that this only happens a Go program is compiled as c-archive or c-shared. If it is
+            // used normally, the signal handlers are installed on each signal by the Go runtime, which
+            // sets SA_ONSTACK and proper alternate stack, hence there's no way the current stack is used
+            // during singal handling.
+            SaFlags::SA_ONSTACK,
+            SigSet::empty(),
+        );
+
+        if let Err(err) = sigaction(signal::SIGUSR1, &sa) {
+            panic!("Failed to set signal handler: {}", err);
+        }
+
+        let main_thread_id = pthread_self();
+        thread::spawn(move || loop {
+            thread::sleep(Duration::from_millis(1));
+            pthread_kill(main_thread_id, SIGUSR1);
+        });
+        test_signal_stack();
+    }
+}
+
+extern "C" fn handler(_: libc::c_int, _: *mut libc::siginfo_t, _: *mut libc::c_void) {
+    // Declare a large local array to use the stack space.
+    let mut large_array: [u8; 1024] = [0; 1024];
+
+    // Use the array to prevent compiler optimizations from removing it.
+    for i in 0..large_array.len() {
+        large_array[i] = i as u8;
+    }
+    if large_array[100] != 100 {
+        panic!("large_array[0] != 0");
+    }
+}
+
+extern "C" {
+    pub fn test_signal_stack();
+}
diff --git a/internal/integration_test/fuzz/wazerolib/lib.go b/internal/integration_test/fuzz/wazerolib/lib.go
index 70fc86dc..18ab080c 100644
--- a/internal/integration_test/fuzz/wazerolib/lib.go
+++ b/internal/integration_test/fuzz/wazerolib/lib.go
@@ -2,14 +2,22 @@ package main
 
 import "C"
 import (
+	"context"
 	"crypto/sha256"
+	_ "embed"
 	"encoding/hex"
 	"fmt"
+	"math"
 	"os"
 	"path"
+	"runtime"
+	"strings"
 
 	"github.com/tetratelabs/wazero"
 	"github.com/tetratelabs/wazero/internal/engine/wazevo"
+	"github.com/tetratelabs/wazero/internal/leb128"
+	"github.com/tetratelabs/wazero/internal/testing/binaryencoding"
+	"github.com/tetratelabs/wazero/internal/wasm"
 )
 
 func main() {}
@@ -81,3 +89,76 @@ func newCompilerConfig() wazero.RuntimeConfig {
 	}
 	return c
 }
+
+//export test_signal_stack
+func test_signal_stack() {
+	if runtime.GOARCH != "arm64" { // TODO: amd64.
+		return
+	}
+	// (module
+	//  (func (export "long_loop")
+	//    (loop
+	//      global.get 0
+	//      i32.eqz
+	//      if  ;; label = @1
+	//        unreachable
+	//      end
+	//      global.get 0
+	//      i32.const 1
+	//      i32.sub
+	//      global.set 0
+	//      br 0
+	//    )
+	//  )
+	//  (global (;0;) (mut i32) i32.const 2147483648)
+	// )
+	bin := binaryencoding.EncodeModule(&wasm.Module{
+		TypeSection:     []wasm.FunctionType{{}},
+		FunctionSection: []wasm.Index{0},
+		GlobalSection: []wasm.Global{{
+			Type: wasm.GlobalType{ValType: wasm.ValueTypeI32, Mutable: true},
+			Init: wasm.ConstantExpression{
+				Opcode: wasm.OpcodeI32Const,
+				Data:   leb128.EncodeInt32(math.MaxInt32),
+			},
+		}},
+		ExportSection: []wasm.Export{{Type: wasm.ExternTypeFunc, Name: "long_loop", Index: 0}},
+		CodeSection: []wasm.Code{
+			{
+				Body: []byte{
+					wasm.OpcodeLoop, 0,
+					wasm.OpcodeGlobalGet, 0,
+					wasm.OpcodeI32Eqz,
+					wasm.OpcodeIf, 0,
+					wasm.OpcodeUnreachable,
+					wasm.OpcodeEnd,
+					wasm.OpcodeGlobalGet, 0,
+					wasm.OpcodeI32Const, 1,
+					wasm.OpcodeI32Sub,
+					wasm.OpcodeGlobalSet, 0,
+					wasm.OpcodeBr, 0,
+					wasm.OpcodeEnd,
+					wasm.OpcodeEnd,
+				},
+			},
+		},
+	})
+	ctx := context.Background()
+	config := wazero.NewRuntimeConfigInterpreter()
+	wazevo.ConfigureWazevo(config)
+	r := wazero.NewRuntimeWithConfig(ctx, config)
+	module, err := r.Instantiate(ctx, bin)
+	if err != nil {
+		panic(err)
+	}
+	defer func() {
+		if err = module.Close(ctx); err != nil {
+			panic(err)
+		}
+	}()
+
+	_, err = module.ExportedFunction("long_loop").Call(ctx)
+	if !strings.Contains(err.Error(), "unreachable") {
+		panic("long_loop should be unreachable")
+	}
+}
diff --git a/internal/sysfs/file_test.go b/internal/sysfs/file_test.go
index b27b2bc4..44e7eb54 100644
--- a/internal/sysfs/file_test.go
+++ b/internal/sysfs/file_test.go
@@ -147,6 +147,9 @@ func TestFileSetAppend(t *testing.T) {
 	require.EqualErrno(t, 0, f.SetAppend(false))
 	require.False(t, f.IsAppend())
 
+	_, errno = f.Seek(0, 0)
+	require.EqualErrno(t, 0, errno)
+
 	// without O_APPEND flag, the data writes at offset zero
 	_, errno = f.Write([]byte("wazero"))
 	require.EqualErrno(t, 0, errno)