diff --git a/.cargo/config.toml b/.cargo/config.toml index 83c365c5..c321c5ef 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -83,3 +83,9 @@ linker = "x86_64-sun-solaris-gcc" # === Windows MSVC (not commonly used in cross-compilation) === [target.x86_64-pc-windows-msvc] # linker = "link.exe" # MSVC requires Visual Studio environment + +[target.aarch64-pc-windows-msvc] +# The native LLVM SDK embeds rpmalloc and is built with the static MSVC CRT. +# A dynamic CRT would supply _strdup while LLVMDisposeMessage calls rpmalloc's +# free. Keep CRT allocation helpers in the same linked allocator domain. +rustflags = ["-C", "target-feature=+crt-static"] diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 275647b5..3925f3ab 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -924,6 +924,12 @@ jobs: cargo build --locked --release --target aarch64-pc-windows-msvc --no-default-features --features llvm-target-aarch64 --jobs 2 + - name: Verify native LLVM message allocator ownership + run: >- + cargo test --locked --release -p llvm --test message_ownership + --target aarch64-pc-windows-msvc --no-default-features + --features llvm-target-aarch64 --jobs 2 + - name: Diagnose native ARM64 code generation id: native_codegen timeout-minutes: 10 @@ -958,6 +964,11 @@ jobs: --no-default-features --features llvm-target-aarch64 --jobs 2 msvc_link_companions_keep_final_names_and_survive_failed_replacement + - name: Run native ARM64 MSVC executor restart regression + run: >- + cargo test --locked --test runtime_regressions --target aarch64-pc-windows-msvc + --no-default-features --features llvm-target-aarch64 --jobs 2 + - name: Install Wave stdlib shell: pwsh run: | diff --git a/llvm/tests/message_ownership.rs b/llvm/tests/message_ownership.rs new file mode 100644 index 00000000..b3895d54 --- /dev/null +++ b/llvm/tests/message_ownership.rs @@ -0,0 +1,49 @@ +//! Exercise C API allocations and their matching LLVM disposal functions. +//! Native Windows SDKs with an integrated allocator must use a compatible CRT. +use inkwell::context::Context; +use inkwell::targets::TargetData; +use llvm_sys::core::{LLVMCreateMessage, LLVMDisposeMessage}; +use std::ffi::{CStr, CString}; + +#[test] +fn llvm_messages_and_target_layouts_share_allocator_ownership() { + #[cfg(all(target_arch = "aarch64", target_os = "windows", target_env = "msvc"))] + assert!( + cfg!(target_feature = "crt-static"), + "the pinned Windows ARM64 LLVM SDK requires the static CRT, including when RUSTFLAGS is set" + ); + // Multiple sizes exercise short strings as well as heap-backed LLVM text. + for length in [0, 1, 15, 16, 127, 4096] { + let text = CString::new("x".repeat(length)).unwrap(); + for _ in 0..32 { + // SAFETY: LLVM copies a live NUL-terminated string. The returned + // allocation is read while live and disposed exactly once by LLVM. + unsafe { + let message = LLVMCreateMessage(text.as_ptr()); + assert!(!message.is_null()); + assert_eq!(CStr::from_ptr(message), text.as_c_str()); + LLVMDisposeMessage(message); + } + } + } + + let context = Context::create(); + let expected = "e-m:w-p:64:64-i64:64-i128:128-n32:64-S128"; + for _ in 0..32 { + let module = context.create_module("allocator-contract"); + let target_data = TargetData::create(expected); + let layout = target_data.get_data_layout(); + module.set_data_layout(&layout); + // This explicit destruction is where #493 crashed on native ARM64. + drop(layout); + drop(target_data); + assert_eq!( + module.get_data_layout().as_str().to_str().unwrap(), + expected + ); + let ir = module.print_to_string(); + assert!(ir.to_str().unwrap().contains(expected)); + drop(ir); + module.verify().unwrap(); + } +} diff --git a/std/process/spawn.wave b/std/process/spawn.wave index 49037bd7..36a142b1 100644 --- a/std/process/spawn.wave +++ b/std/process/spawn.wave @@ -66,21 +66,88 @@ pub struct ProcSpawnStdoutResult { read_fd: i64; } -pub fun _proc_dup_child_fd(src_fd: i64, dst_fd: i64) -> i64 { - if (src_fd < 0) { - return 0; +// dup may return a closed standard descriptor. Hold those slots until a +// duplicate outside 0..2 is available, then restore the closed slots. +fun _proc_save_child_fd(source: i64) -> i64 { + var low: array; + var count: i64 = 0; + var saved: i64 = io_dup(source); + while (saved >= 0 && saved <= IO_STDERR_FD) { + low[count] = saved; + count += 1; + saved = io_dup(source); } - - if (src_fd == dst_fd) { - return 0; + var i: i64 = 0; + while (i < count) { + io_close(low[i]); + i += 1; } + return saved; +} - var r: i64 = io_dup2(src_fd, dst_fd); - if (r < 0) { - return r; +pub fun _proc_remap_child_fds(stdin_fd: i64, stdout_fd: i64, stderr_fd: i64) -> i64 { + var sources: array = [stdin_fd, stdout_fd, stderr_fd]; + var saved: array = [-1, -1, -1]; + var result: i64 = 0; + var i: i64 = 0; + // Validate before retaining duplicates: an allocated temporary must not + // make an originally closed source number appear valid later in the plan. + while (i < 3) { + if (sources[i] >= 0 && sources[i] != i) { + var probe: i64 = io_dup(sources[i]); + if (probe < 0) { + return probe; + } + io_close(probe); + } + i += 1; + } + i = 0; + // Snapshot every changing source before any destination can overwrite it. + while (i < 3 && result >= 0) { + if (sources[i] >= 0 && sources[i] != i) { + saved[i] = _proc_save_child_fd(sources[i]); + if (saved[i] < 0) { + result = saved[i]; + } + } + i += 1; + } + i = 0; + while (i < 3 && result >= 0) { + if (saved[i] >= 0) { + result = io_dup2(saved[i], i); + } + i += 1; + } + i = 0; + while (i < 3) { + if (saved[i] >= 0) { + io_close(saved[i]); + } + i += 1; + } + if (result < 0) { + return result; + } + // Shared sources stay open through all mappings and are closed once. + i = 0; + while (i < 3) { + if (sources[i] > IO_STDERR_FD) { + var first: bool = true; + var previous: i64 = 0; + while (previous < i) { + if (sources[previous] == sources[i]) { + first = false; + } + previous += 1; + } + if (first) { + io_close(sources[i]); + } + } + i += 1; } - - io_close(src_fd); return 0; } @@ -98,15 +165,7 @@ pub fun proc_spawn_exec_raw( } if (pid == 0) { - if (_proc_dup_child_fd(stdin_fd, IO_STDIN_FD) < 0) { - proc_exit(PROC_EXIT_DUP_FAIL); - } - - if (_proc_dup_child_fd(stdout_fd, IO_STDOUT_FD) < 0) { - proc_exit(PROC_EXIT_DUP_FAIL); - } - - if (_proc_dup_child_fd(stderr_fd, IO_STDERR_FD) < 0) { + if (_proc_remap_child_fds(stdin_fd, stdout_fd, stderr_fd) < 0) { proc_exit(PROC_EXIT_DUP_FAIL); } diff --git a/std/sys/windows/completion_port.wave b/std/sys/windows/completion_port.wave new file mode 100644 index 00000000..285de5d3 --- /dev/null +++ b/std/sys/windows/completion_port.wave @@ -0,0 +1,114 @@ +// This file is part of the Wave language project. +// SPDX-License-Identifier: Apache-2.0 + +// A socket stays associated with its completion port until closesocket. +// The executor owns another reference until cancellation packets are drained. +// Access is confined to the cooperative executor thread, including socket close. +import("std::mem::alloc")::{mem_alloc_zeroed, mem_free}; + +extern(system, "CreateIoCompletionPort") fun win_create_port( + file: ptr, port: ptr, key: u64, threads: u32 +) -> ptr; +extern(system, "CloseHandle") fun win_close_port(handle: ptr) -> i32; +extern(system, "GetLastError") fun win_port_error() -> u32; + +struct Association { + socket: i64; + next: ptr; +} +// Both supported Windows ABIs are 64-bit: i64 followed by one 64-bit pointer. +const ASSOCIATION_SIZE: i64 = 16; +static _port: ptr = null; +static _executor_active: bool = false; +static _sockets: ptr = null; + +pub fun completion_port_handle() -> ptr { + return _port; +} + +pub fun completion_port_acquire() -> i64 { + if (_port == null) { + _port = win_create_port(-1 as ptr, null, 0, 1); + if (_port == null) { + return -(win_port_error() as i64); + } + } + _executor_active = true; + return 0; +} + +fun _close_if_unused() { + if (!_executor_active && _sockets == null && _port != null) { + // Keep ownership on failure so another release can retry it. + if (win_close_port(_port) != 0) { + _port = null; + } + } +} + +pub fun completion_port_associate(socket: i64) -> i64 { + var status: i64 = completion_port_acquire(); + if (status < 0) { + return status; + } + var existing: ptr = _sockets; + while (existing != null) { + if (existing.socket == socket) { + return 0; + } + existing = existing.next; + } + // Allocate before associating: an allocation failure must not leave an + // untracked socket irreversibly bound to this port. + var node: ptr = mem_alloc_zeroed(ASSOCIATION_SIZE) as ptr; + if (node == null) { + return -12; + } + if (win_create_port(socket as ptr, _port, 0, 1) == null) { + var error: i64 = -(win_port_error() as i64); + mem_free(node as ptr, ASSOCIATION_SIZE); + return error; + } + node.socket = socket; + node.next = _sockets; + _sockets = node; + return 0; +} + +// Called only after Winsock has successfully closed the socket. +pub fun completion_port_socket_closed(socket: i64) { + var previous: ptr = null; + var node: ptr = _sockets; + while (node != null) { + if (node.socket == socket) { + if (previous == null) { + _sockets = node.next; + } else { + previous.next = node.next; + } + mem_free(node as ptr, ASSOCIATION_SIZE); + _close_if_unused(); + return; + } + previous = node; + node = node.next; + } +} + +// Successful final WSACleanup closes the process's remaining Winsock sockets. +pub fun completion_port_sockets_closed() { + var node: ptr = _sockets; + while (node != null) { + var next: ptr = node.next; + mem_free(node as ptr, ASSOCIATION_SIZE); + node = next; + } + _sockets = null; + _close_if_unused(); +} + +// The caller must drain all operations and join wait callbacks before release. +pub fun completion_port_release_executor() { + _executor_active = false; + _close_if_unused(); +} diff --git a/std/sys/windows/iocp.wave b/std/sys/windows/iocp.wave index 590d446a..f998b39a 100644 --- a/std/sys/windows/iocp.wave +++ b/std/sys/windows/iocp.wave @@ -3,6 +3,10 @@ import("std::mem::alloc")::{mem_alloc_zeroed, mem_free}; import("std::mem::ops")::{mem_copy}; import("std::process::core")::{proc_exit}; +import("std::sys::windows::completion_port")::{ + completion_port_handle, completion_port_acquire, + completion_port_associate, completion_port_release_executor, +}; struct Overlapped { internal: u64; internal_high: u64; offset: u32; offset_high: u32; event: ptr; } struct WsaBuffer { length: u32; data: ptr; } @@ -16,13 +20,10 @@ struct Operation { next: ptr; } pub struct IocpCompletion { token: i64; value: i64; kind: i32; } -static _port: ptr = null; static _operations: ptr = null; -extern(system,"CreateIoCompletionPort") fun win_create_port(file:ptr,port:ptr,key:u64,threads:u32)->ptr; extern(system,"GetQueuedCompletionStatus") fun win_dequeue(port:ptr,bytes:ptr,key:ptr,overlapped:ptr>,timeout:u32)->i32; extern(system,"PostQueuedCompletionStatus") fun win_post(port:ptr,bytes:u32,key:u64,overlapped:ptr)->i32; extern(system,"CancelIoEx") fun win_cancel(file:ptr,overlapped:ptr)->i32; -extern(system,"CloseHandle") fun win_close(handle:ptr)->i32; extern(system,"GetLastError") fun win_error()->u32; extern(system,"WSAGetLastError") fun wsa_error()->i32; extern(system,"WSASend") fun wsa_send(socket:u64,buffers:ptr,count:u32,bytes:ptr,flags:u32,overlapped:ptr,callback:ptr)->i32; @@ -34,11 +35,6 @@ extern(system,"WSAEnumNetworkEvents") fun wsa_events(socket:u64,event:ptr,ev extern(system,"RegisterWaitForSingleObject") fun win_register(wait:ptr>,event:ptr,callback:ptr,context:ptr,timeout:u32,flags:u32)->i32; extern(system,"UnregisterWaitEx") fun win_unregister(wait:ptr,completion:ptr)->i32; -fun _ensure_port()->i64 { - if (_port==null) { _port=win_create_port(-1 as ptr,null,0,1); } - if (_port==null) { return -(win_error() as i64); } - return 0; -} fun _link(op:ptr) { op.next=_operations; _operations=op; } fun _unlink(op:ptr) { var previous:ptr =null; var cursor:ptr =_operations; @@ -68,12 +64,11 @@ export(system,"__wave_task_windows_notify") fun _notify(context:ptr,timed_ou } pub fun iocp_start_io(token:i64,socket:i64,destination:ptr,length:i64,writing:bool)->i64 { if(socket<0||destination==null||length<=0||length>4294967295){return -22;} - var status:i64=_ensure_port();if(status<0){return status;} - if(win_create_port(socket as ptr,_port,0,1)==null){return -(win_error() as i64);} + var status:i64=completion_port_associate(socket);if(status<0){return status;} var op:ptr =__wave_async_alloc(); op.storage=mem_alloc_zeroed(length); if(op.storage==null){__wave_async_free_slot(op);return -12;} - op.port=_port;op.token=token;op.socket=socket;op.kind=0;op.destination=destination;op.length=length;op.writing=writing; + op.port=completion_port_handle();op.token=token;op.socket=socket;op.kind=0;op.destination=destination;op.length=length;op.writing=writing; op.buffer=WsaBuffer{length:length as u32,data:op.storage}; if(writing){mem_copy(op.storage,destination,length);} var started:i32=0; @@ -84,9 +79,9 @@ pub fun iocp_start_io(token:i64,socket:i64,destination:ptr,length:i64,writin _link(op);return 0; } pub fun iocp_watch(token:i64,socket:i64,interests:i32)->i64 { - var status:i64=_ensure_port();if(status<0){return status;} + var status:i64=completion_port_acquire();if(status<0){return status;} var op:ptr =__wave_async_alloc(); - op.port=_port;op.token=token;op.socket=socket;op.kind=1;op.interests=interests;op.event=wsa_create_event(); + op.port=completion_port_handle();op.token=token;op.socket=socket;op.kind=1;op.interests=interests;op.event=wsa_create_event(); if(op.event==null){var code:i64=-(wsa_error() as i64);__wave_async_free_slot(op);return code;} var mask:i32=32; if((interests&1)!=0){mask=mask|1|8;} @@ -115,9 +110,10 @@ pub fun iocp_cancel(token:i64)->i32 { // 1 = completion, 0 = wait timeout, negative = port error. pub fun iocp_wait(output:ptr,timeout_ms:i32)->i64 { if(output==null||timeout_ms< -1){return -22;} - if(_port==null){return 0;} + var port:ptr =completion_port_handle(); + if(port==null){return 0;} var bytes:u32=0;var key:u64=0;var pointer:ptr =null; - var ok:i32=win_dequeue(_port,&bytes,&key,&pointer,timeout_ms as u32); + var ok:i32=win_dequeue(port,&bytes,&key,&pointer,timeout_ms as u32); var code:u32=0;if(ok==0){code=win_error();} if(pointer==null){if(code==258){return 0;}return -(code as i64);} var op:ptr =_operations; @@ -153,5 +149,5 @@ pub fun iocp_shutdown() { var result:IocpCompletion; if(iocp_wait(&result,-1)<0){proc_exit(73);} } - if(_port!=null){win_close(_port);_port=null;} + completion_port_release_executor(); } diff --git a/std/sys/windows/socket.wave b/std/sys/windows/socket.wave index ca096eee..0a6d4a98 100644 --- a/std/sys/windows/socket.wave +++ b/std/sys/windows/socket.wave @@ -4,6 +4,9 @@ // SPDX-License-Identifier: Apache-2.0 // Windows Winsock provider. Calls retain SOCKET values in i64. +import("std::sys::windows::completion_port")::{ + completion_port_socket_closed, completion_port_sockets_closed, +}; extern(system, "WSAStartup") fun win_wsa_startup(version: u16, data: ptr) -> i32; extern(system, "WSACleanup") fun win_wsa_cleanup() -> i32; @@ -96,6 +99,7 @@ pub fun socket_runtime_cleanup() -> i64 { var result: i32 = win_wsa_cleanup(); if (result != 0) { return -(win_wsa_last_error() as i64); } WINSOCK_STATE = 0; + completion_port_sockets_closed(); return 0; } @@ -135,7 +139,11 @@ pub fun shutdown(fd: i64, how: i32) -> i64 { return winsock_result(win_shutdown(fd, how) as i64); } pub fun close_socket(fd: i64) -> i64 { - return winsock_result(win_close_socket(fd) as i64); + var result: i64 = winsock_result(win_close_socket(fd) as i64); + if (result == 0) { + completion_port_socket_closed(fd); + } + return result; } pub fun socket_set_nonblock(fd: i64, enabled: i32) -> i64 { var value: u32 = 0; diff --git a/std/task.wave b/std/task.wave index 949616b0..41d2aa31 100644 --- a/std/task.wave +++ b/std/task.wave @@ -1,5 +1,7 @@ // Cooperative async tasks. Futures are lazy and single-consumer handles. // Complete or cancel outstanding work, then call shutdown to release the executor. +// Windows sockets remain usable across shutdown; their IOCP is released after +// the last associated socket is closed through std::net (on the executor thread). import("std::mem::alloc")::{mem_alloc_zeroed, mem_free}; import("std::process::core")::{proc_exit}; import("std::net::event")::{NetEventLoop, NetEvent, NetEventWaitResult, net_event_loop_create, net_event_loop_close, net_event_add, net_event_remove, net_event_wait}; diff --git a/tests/codegen_regressions.rs b/tests/codegen_regressions.rs index 3326ac91..4c192f9b 100644 --- a/tests/codegen_regressions.rs +++ b/tests/codegen_regressions.rs @@ -6522,3 +6522,44 @@ fn darwin_pipe_captures_both_kernel_return_registers() { } fs::remove_dir_all(dir).unwrap(); } + +#[test] +fn executor_restart_fixture_emits_for_windows_gnu_and_msvc() { + let dir = temp_case_dir("executor-restart-targets"); + let home = dir.join("home"); + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + copy_tree(&root.join("std"), &home.join(".wave/lib/wave/std")); + for target in [ + "x86_64-pc-windows-gnu", + "x86_64-pc-windows-msvc", + "aarch64-pc-windows-gnu", + "aarch64-pc-windows-msvc", + ] { + if llvm::codegen::target::target_spec_for_triple(target).is_none() { + continue; + } + let output = dir.join(format!("{target}.o")); + let compiled = wavec_command() + .env("HOME", &home) + .env("USERPROFILE", &home) + .arg("build") + .arg(root.join("tests/fixtures/async/executor_restart.wave")) + .args(["--target", target, "--emit=obj", "-o"]) + .arg(&output) + .output() + .unwrap(); + assert!( + compiled.status.success(), + "{target}: {}", + String::from_utf8_lossy(&compiled.stderr) + ); + let object = fs::read(output).unwrap(); + let expected = if target.starts_with("aarch64") { + 0xaa64 + } else { + 0x8664 + }; + assert_eq!(u16::from_le_bytes([object[0], object[1]]), expected); + } + fs::remove_dir_all(dir).unwrap(); +} diff --git a/tests/fixtures/async/executor_restart.wave b/tests/fixtures/async/executor_restart.wave new file mode 100644 index 00000000..cc890031 --- /dev/null +++ b/tests/fixtures/async/executor_restart.wave @@ -0,0 +1,177 @@ +import("std::task" as task); +import("std::task::net" as net); +import("std::net::tcp")::{TcpListener, tcp_bind_loopback, tcp_listener_local_addr_v4}; +import("std::net::address")::{SocketAddrV4}; +import("std::net::error")::{NetResult, NetIoResult}; +import("std::net::socket_base")::{net_socket_tcp_v4}; +#[target(os="windows")] +import("std::sys::windows::completion_port")::{completion_port_handle}; + +static peer: i64 = -1; +static client: i64 = -1; + +#[target(os="windows")] +fun port_present() -> bool { + return completion_port_handle() != null; +} +#[target(os="linux")] +fun port_present() -> bool { + return false; +} + +async fun accept_peer(listener: i64) -> i32 { + peer = await net::accept(listener, 2000); + if (peer < 0) { + return 20; + } + return 0; +} + +async fun connect_client(address: SocketAddrV4) -> i32 { + client = net_socket_tcp_v4(); + if (client < 0) { + return 21; + } + var status: i64 = await net::connect(client, address, 2000); + if (status < 0) { + return 22; + } + return 0; +} + +async fun open_pair(listener: i64, address: SocketAddrV4) -> i32 { + var accepting: Future = task::spawn(accept_peer(listener)); + var connecting: Future = task::spawn(connect_client(address)); + var a: i32 = await accepting; + var b: i32 = await connecting; + if (a != 0) { + return a; + } + return b; +} + +async fun echo_packet() -> i32 { + var packet: array; + var offset: i64 = 0; + while (offset < 64) { + var received: NetIoResult = await net::read(peer, &packet[0] + offset, 64 - offset, 2000); + if (received.error.kind != 0 || received.eof) { + return 30; + } + offset += received.count; + } + var sent: NetIoResult = await net::write_all(peer, &packet[0], 64, 2000); + if (sent.error.kind != 0 || sent.count != 64) { + return 31; + } + return 0; +} + +async fun exchange_packet(round: i32) -> i32 { + var packet: array; + var i: i64 = 0; + while (i < 64) { + packet[i] = (i * 7 + round as i64) as u8; + i += 1; + } + var sent: NetIoResult = await net::write_all(client, &packet[0], 64, 2000); + if (sent.error.kind != 0 || sent.count != 64) { + return 32; + } + var offset: i64 = 0; + while (offset < 64) { + var received: NetIoResult = await net::read(client, &packet[0] + offset, 64 - offset, 2000); + if (received.error.kind != 0 || received.eof) { + return 33; + } + offset += received.count; + } + i = 0; + while (i < 64) { + if (packet[i] != (i * 7 + round as i64) as u8) { + return 34; + } + i += 1; + } + return 0; +} + +async fun exchange(round: i32) -> i32 { + var echoing: Future = task::spawn(echo_packet()); + var exchanging: Future = task::spawn(exchange_packet(round)); + var a: i32 = await echoing; + var b: i32 = await exchanging; + if (a != 0) { + return a; + } + return b; +} + +async fun cancel_idle_read() { + var buffer: array; + var reading: Future = task::spawn(net::read(client, &buffer[0], 8, -1)); + await task::yield_now(); + await task::cancel_and_join(reading); +} + +fun exercise(close_before_shutdown: bool) -> i32 { + var listener: NetResult = tcp_bind_loopback(0); + if (!listener.ok) { + return 10; + } + var address: NetResult = tcp_listener_local_addr_v4(listener.value); + if (!address.ok) { + return 11; + } + var opened: i32 = task::block_on(open_pair(listener.value.fd, address.value)); + if (opened != 0) { + return opened; + } + net::close(listener.value.fd); + var round: i32 = 0; + while (round < 8) { + var status: i32 = task::block_on(exchange(round)); + if (status != 0) { + return status; + } + task::block_on(cancel_idle_read()); + var retained: bool = port_present(); + task::shutdown(); + task::shutdown(); + if ((retained && !port_present()) || (!retained && port_present())) { + return 40; + } + round += 1; + } + if (close_before_shutdown) { + var status: i32 = task::block_on(exchange(9)); + if (status != 0) { + return status; + } + } + if (net::close(client) != 0 || net::close(peer) != 0) { + return 41; + } + if (!close_before_shutdown && port_present()) { + return 42; + } + task::shutdown(); + task::shutdown(); + if (port_present()) { + return 43; + } + return 0; +} + +fun main() -> i32 { + var iteration: i32 = 0; + while (iteration < 4) { + var status: i32 = exercise(iteration % 2 == 0); + if (status != 0) { + println("restart iteration {} failed: {}", iteration, status); + return status; + } + iteration += 1; + } + return 0; +} diff --git a/tests/fixtures/process/descriptor_remapping.wave b/tests/fixtures/process/descriptor_remapping.wave new file mode 100644 index 00000000..7c63b2fe --- /dev/null +++ b/tests/fixtures/process/descriptor_remapping.wave @@ -0,0 +1,179 @@ +import("std::io::fd")::{io_pipe, io_close, io_dup, io_dup2, io_write_all, io_read_exact}; +import("std::process::core")::{proc_fork, proc_exit, proc_waitpid_raw}; +import("std::process::spawn")::{proc_spawn_exec_raw, _proc_remap_child_fds}; + +fun shared_spawn() -> i32 { + var pipe: array; + if (io_pipe(&pipe[0]) < 0) { + return 10; + } + var argv: array, 4> = [ + "/bin/sh" as ptr, "-c" as ptr, + "printf A; printf B >&2" as ptr, null + ]; + var envp: array, 1> = [null]; + var child: i64 = proc_spawn_exec_raw("/bin/sh", &argv[0], &envp[0], -1, pipe[1] as i64, pipe[1] as i64); + if (child < 0) { + return 11; + } + var status: i32 = 0; + if (proc_waitpid_raw(child, &status, 0) != child || status != 0) { + return 12; + } + // The parent's source remains owned by the parent after spawning. + if (io_write_all(pipe[1] as i64, "C" as ptr, 1) != 1) { + return 13; + } + io_close(pipe[1] as i64); + var data: array; + if (io_read_exact(pipe[0] as i64, &data[0], 3) != 3) { + return 14; + } + io_close(pipe[0] as i64); + if (data[0] != 65 || data[1] != 66 || data[2] != 67) { + return 15; + } + return 0; +} + +fun cycle(swapping: bool) -> i32 { + var a: array; + var b: array; + var c: array; + if (io_pipe(&a[0]) < 0 || io_pipe(&b[0]) < 0 || io_pipe(&c[0]) < 0) { + return 20; + } + if (io_dup2(a[1] as i64, 0) < 0 || io_dup2(b[1] as i64, 1) < 0 || io_dup2(c[1] as i64, 2) < 0) { + return 21; + } + io_close(a[1] as i64); + io_close(b[1] as i64); + io_close(c[1] as i64); + var result: i64 = 0; + if (swapping) { + result = _proc_remap_child_fds(1, 0, 2); + } else { + result = _proc_remap_child_fds(1, 2, 0); + } + if (result < 0) { + return 22; + } + if (io_write_all(0, "A" as ptr, 1) != 1 || + io_write_all(1, "B" as ptr, 1) != 1 || + io_write_all(2, "C" as ptr, 1) != 1) { + return 23; + } + var x: u8 = 0; + var y: u8 = 0; + var z: u8 = 0; + if (io_read_exact(a[0] as i64, &x, 1) != 1 || + io_read_exact(b[0] as i64, &y, 1) != 1 || + io_read_exact(c[0] as i64, &z, 1) != 1) { + return 24; + } + io_close(a[0] as i64); + io_close(b[0] as i64); + io_close(c[0] as i64); + if (swapping) { + if (x != 66 || y != 65 || z != 67) { + return 25; + } + } else if (x != 67 || y != 65 || z != 66) { + return 26; + } + return 0; +} + +fun closed_standard_slots() -> i32 { + var pipe: array; + if (io_pipe(&pipe[0]) < 0) { + return 30; + } + io_close(0); + io_close(1); + io_close(2); + if (_proc_remap_child_fds(-1, pipe[1] as i64, pipe[1] as i64) < 0) { + return 31; + } + if (io_dup(0) >= 0 || io_dup(pipe[1] as i64) >= 0) { + return 32; + } + if (io_write_all(1, "D" as ptr, 1) != 1 || io_write_all(2, "E" as ptr, 1) != 1) { + return 33; + } + var data: array; + if (io_read_exact(pipe[0] as i64, &data[0], 2) != 2 || data[0] != 68 || data[1] != 69) { + return 34; + } + io_close(pipe[0] as i64); + return 0; +} + +fun failed_snapshot() -> i32 { + var pipe: array; + if (io_pipe(&pipe[0]) < 0) { + return 40; + } + var next: i64 = io_dup(pipe[1] as i64); + if (next < 0) { + return 41; + } + io_close(next); + if (_proc_remap_child_fds(-1, pipe[1] as i64, next) >= 0) { + return 47; + } + if (_proc_remap_child_fds(-1, pipe[1] as i64, 2147483647) >= 0) { + return 42; + } + var after: i64 = io_dup(pipe[1] as i64); + if (after != next) { + return 43; + } + io_close(after); + if (_proc_remap_child_fds(-1, -1, -1) != 0 || _proc_remap_child_fds(0, 1, 2) != 0) { + return 44; + } + if (io_write_all(pipe[1] as i64, "F" as ptr, 1) != 1) { + return 45; + } + var data: u8 = 0; + if (io_read_exact(pipe[0] as i64, &data, 1) != 1 || data != 70) { + return 46; + } + io_close(pipe[0] as i64); + io_close(pipe[1] as i64); + return 0; +} + +fun main() -> i32 { + var scenario: i32 = 0; + while (scenario < 5) { + var child: i64 = proc_fork(); + if (child < 0) { + return 50; + } + if (child == 0) { + if (scenario == 0) { + proc_exit(shared_spawn()); + } else if (scenario == 1) { + proc_exit(cycle(true)); + } else if (scenario == 2) { + proc_exit(cycle(false)); + } else if (scenario == 3) { + proc_exit(closed_standard_slots()); + } else { + proc_exit(failed_snapshot()); + } + } + var status: i32 = 0; + if (proc_waitpid_raw(child, &status, 0) != child) { + return 51; + } + if (status != 0) { + println("descriptor scenario {} failed: {}", scenario, status); + return 52; + } + scenario += 1; + } + return 0; +} diff --git a/tests/runtime_regressions.rs b/tests/runtime_regressions.rs new file mode 100644 index 00000000..9a65a7c2 --- /dev/null +++ b/tests/runtime_regressions.rs @@ -0,0 +1,103 @@ +//! Native standard-library regressions with bounded process-tree execution. +#![cfg(any(target_os = "linux", target_os = "windows"))] + +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::atomic::{AtomicU64, Ordering}; + +static NEXT_FIXTURE: AtomicU64 = AtomicU64::new(0); + +struct FixtureDirectory(PathBuf); + +impl Drop for FixtureDirectory { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +fn copy_tree(source: &Path, destination: &Path) { + fs::create_dir_all(destination).unwrap(); + for entry in fs::read_dir(source).unwrap() { + let entry = entry.unwrap(); + let destination = destination.join(entry.file_name()); + if entry.file_type().unwrap().is_dir() { + copy_tree(&entry.path(), &destination); + } else { + fs::copy(entry.path(), destination).unwrap(); + } + } +} + +fn run_native_fixture(name: &str) { + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let compiler = PathBuf::from(env!("CARGO_BIN_EXE_wavec")); + let target = Command::new(&compiler) + .args(["print", "default-target"]) + .output() + .unwrap(); + assert!(target.status.success()); + let mut host = String::from_utf8(target.stdout).unwrap().trim().to_owned(); + if cfg!(all(windows, target_env = "msvc")) { + host = host.replace("-windows-gnu", "-windows-msvc"); + } + if llvm::codegen::target::target_spec_for_triple(&host).is_none() { + eprintln!("native runtime fixture skipped: LLVM target {host} is disabled"); + return; + } + let sequence = NEXT_FIXTURE.fetch_add(1, Ordering::Relaxed); + let directory = FixtureDirectory( + std::env::temp_dir().join(format!("wave-runtime-{}-{sequence}", std::process::id())), + ); + fs::create_dir_all(&directory.0).unwrap(); + let home = directory.0.join("home"); + copy_tree(&root.join("std"), &home.join(".wave/lib/wave/std")); + let executable = directory.0.join(if cfg!(windows) { + "fixture.exe" + } else { + "fixture" + }); + let output = Command::new(&compiler) + .env("HOME", &home) + .env("USERPROFILE", &home) + .args(["build", "--target", &host]) + .arg(root.join("tests/fixtures").join(name)) + .arg("-o") + .arg(&executable) + .output() + .unwrap(); + assert!( + output.status.success(), + "{name} compile: {}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + // The fixture forks children: bound and reap the whole tree on failure. + let output = Command::new(if cfg!(windows) { "python" } else { "python3" }) + .current_dir(&root) + .args([ + "-c", + "import sys; from tools.process_tree import run_process; r = run_process([sys.argv[1]], timeout=20); raise SystemExit(r.returncode)", + ]) + .arg(executable) + .output() + .unwrap(); + assert!( + output.status.success(), + "{name} runtime: {}\n{}\n{}", + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} + +#[cfg(target_os = "linux")] +#[test] +fn child_standard_streams_preserve_shared_and_cyclic_descriptors() { + run_native_fixture("process/descriptor_remapping.wave"); +} + +#[test] +fn sockets_survive_executor_restart_and_release_their_completion_port() { + run_native_fixture("async/executor_restart.wave"); +} diff --git a/tools/provision_windows_arm64_libxml2.ps1 b/tools/provision_windows_arm64_libxml2.ps1 index 45ed3d95..d46b3810 100644 --- a/tools/provision_windows_arm64_libxml2.ps1 +++ b/tools/provision_windows_arm64_libxml2.ps1 @@ -69,12 +69,14 @@ if ($systemLibraries -notmatch '\bxml2s\.lib\b') { throw "The pinned LLVM SDK no longer requests xml2s.lib; review this provisioning contract" } -# /MD matches Rust's default MSVC CRT. No zlib, lzma, iconv, Python or DLL +# /MT matches the pinned LLVM SDK and the ARM64 Rust target configuration. +# LLVM embeds rpmalloc: mixing dynamic-CRT allocation helpers with its free +# corrupts ownership of LLVM messages. No zlib, lzma, iconv, Python or DLL # dependencies are introduced. Keep the pre-2.14 XML ABI used by LLVM 21. Invoke-Checked "cmake" @("-S", $source, "-B", $build, "-G", "Ninja", "-DCMAKE_BUILD_TYPE=Release", "-DCMAKE_INSTALL_PREFIX=$install", "-DCMAKE_C_COMPILER=$clang", "-DCMAKE_C_COMPILER_TARGET=aarch64-pc-windows-msvc", - "-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDLL", "-DBUILD_SHARED_LIBS=OFF", + "-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded", "-DBUILD_SHARED_LIBS=OFF", "-DLIBXML2_WITH_ICONV=OFF", "-DLIBXML2_WITH_ICU=OFF", "-DLIBXML2_WITH_LZMA=OFF", "-DLIBXML2_WITH_ZLIB=OFF", "-DLIBXML2_WITH_PYTHON=OFF", "-DLIBXML2_WITH_PROGRAMS=OFF", "-DLIBXML2_WITH_TESTS=OFF", "-DLIBXML2_WITH_FTP=OFF", "-DLIBXML2_WITH_HTTP=OFF", @@ -88,7 +90,9 @@ Copy-Item (Join-Path $libDir "libxml2s.lib") $library -Force Assert-Arm64Archive $library (Join-Path $llvmBin "llvm-readobj.exe") # llvm-sys supplies xml2s; libxml2's Windows entropy/socket helpers also use -# these Windows SDK import libraries. Preserve pre-existing Rust flags. +# these Windows SDK import libraries. RUSTFLAGS overrides target.rustflags in +# Cargo configuration, so repeat the required static CRT flag here as well. +# Preserve other pre-existing flags. "LIB=$libDir;$env:LIB" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append -"RUSTFLAGS=$env:RUSTFLAGS -l bcrypt -l ws2_32" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append +"RUSTFLAGS=$env:RUSTFLAGS -C target-feature=+crt-static -l bcrypt -l ws2_32" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append "WAVE_LIBXML2_LICENSE=$source\Copyright" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append