Merge commit '5a066f67' into merge/unity

This commit is contained in:
velzie committed 2026-10-07 11:51:37 -04:00
commit ba7d2dc7c0
31 files changed
+1928 -94

No files matched your search

+8
View File
@@ -172,6 +172,11 @@ pub mod raw {
pub const SETUID: u64 = 213;
pub const SETGID: u64 = 214;
pub const MADVISE: u64 = 219;
pub const MLOCK: u64 = 150;
pub const UNSHARE: u64 = 310;
pub const MUNLOCK: u64 = 151;
pub const MLOCKALL: u64 = 152;
pub const MUNLOCKALL: u64 = 153;
pub const GETDENTS64: u64 = 220;
pub const FCNTL64: u64 = 221;
pub const GETTID: u64 = 224;
@@ -824,6 +829,8 @@ pub fn translate(
x::PRCTL => nr::PRCTL,
x::ARCH_PRCTL => nr::ARCH_PRCTL,
x::PERSONALITY => nr::NOOP_OK,
// Advisory, as for x86-64: memory is never paged out.
x::MLOCK | x::MUNLOCK | x::MLOCKALL | x::MUNLOCKALL => nr::NOOP_OK,
x::UNAME => nr::UNAME,
x::SYSINFO => nr::SYSINFO,
x::GETRANDOM => nr::GETRANDOM,
@@ -881,6 +888,7 @@ pub fn translate(
}
x::SET_TID_ADDRESS => nr::SET_TID_ADDRESS,
x::SET_ROBUST_LIST => nr::SET_ROBUST_LIST,
x::UNSHARE => nr::UNSHARE,
x::GET_ROBUST_LIST => nr::GET_ROBUST_LIST,
x::RSEQ => nr::RSEQ,
x::MEMBARRIER => nr::MEMBARRIER,
+7
View File
@@ -533,6 +533,13 @@ impl MemoryMap {
self.brk_grow(addr).0
}
/// Hand out no mapping above `top` unless asked for there (a process
/// hosting Windows code, W3: Windows keeps 0x7ffe0000 for
/// KUSER_SHARED_DATA and the top of user space for itself).
pub fn set_mmap_top(&mut self, top: u64) {
self.mmap_next = self.mmap_next.min(page_align_down(top));
}
/// Find and reserve a free range of `len` bytes. Top-down from [`MMAP_TOP`],
/// matching Linux, so the heap and mappings grow apart.
pub fn mmap(&mut self, addr: u64, len: u64, prot: u32, flags: u64, name: &str) -> SysResult {
+342 -14
View File
@@ -244,11 +244,82 @@ impl Transport for LoopEnd {
}
}
/// One end of a TCP connection between guests in different processes of a
/// page: a stream in the global kernel (`unix::Remote`), named by the port
/// (`tcp_name`), each end an id there.
pub struct RemoteEnd {
id: i32,
remote: crate::unix::Remote,
}
impl std::fmt::Debug for RemoteEnd {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "RemoteEnd({})", self.id)
}
}
impl Transport for RemoteEnd {
fn connect(&self, _host: &str, _port: u16) -> Result<(), Errno> {
Ok(())
}
fn status(&self) -> Status {
Status::Open
}
fn send(&self, data: &[u8]) -> Result<usize, Errno> {
match (self.remote.write)(self.id, data.as_ptr() as crate::PAddr, data.len() as u32) {
n if n >= 0 => Ok(n as usize),
n => Err(Errno(-n)),
}
}
fn recv(&self, buf: &mut [u8]) -> Result<usize, Errno> {
match (self.remote.read)(self.id, buf.as_mut_ptr() as crate::PAddr, buf.len() as u32) {
n if n >= 0 => Ok(n as usize),
n => Err(Errno(-n)),
}
}
fn has_data(&self) -> bool {
(self.remote.readable)(self.id) != 0
}
fn available(&self) -> usize {
(self.remote.available)(self.id).max(0) as usize
}
fn close(&self) {
(self.remote.close)(self.id);
}
}
/// The global kernel's name for a TCP port (abstract, so no file appears).
fn tcp_name(port: u16) -> String {
format!("\0gex-tcp:{port}")
}
/// The global kernel's name for a UDP port. The global kernel has only
/// streams, so datagrams between a page's processes travel framed over one
/// stream per (sender, port): a 4-byte length and a 2-byte source port
/// (little-endian), then the bytes.
fn udp_name(port: u16) -> String {
format!("\0gex-udp:{port}")
}
/// A UDP socket's side of the global kernel (`Socket::udp_remote`).
#[derive(Debug, Default)]
struct UdpRemote {
/// The listener its port is bound as.
listener: Option<i32>,
/// Accepted streams from senders, each with the bytes read from it that
/// do not yet make a whole datagram.
inbound: Vec<(i32, Vec<u8>)>,
/// Streams to the ports this socket has sent to.
outbound: HashMap<u16, i32>,
}
/// The TCP ports guests on this machine listen on. Shared across `fork`, as
/// the resolver is: a child connects to its parent's listener.
#[derive(Debug, Default)]
pub struct Ports {
listeners: Mutex<HashMap<u16, std::sync::Weak<Socket>>>,
/// Bound UDP sockets, by port: where a datagram to the loopback goes.
udp: Mutex<HashMap<u16, std::sync::Weak<Socket>>>,
next_ephemeral: AtomicU32,
}
@@ -275,6 +346,20 @@ impl Ports {
fn listener(&self, port: u16) -> Option<Arc<Socket>> {
self.listeners.lock().unwrap().get(&port).and_then(|w| w.upgrade())
}
fn udp_at(&self, port: u16) -> Option<Arc<Socket>> {
self.udp.lock().unwrap().get(&port).and_then(|w| w.upgrade())
}
/// A UDP port nothing is bound to.
fn udp_ephemeral(&self) -> u16 {
loop {
let p = self.ephemeral();
if self.udp_at(p).is_none() {
return p;
}
}
}
}
/// Whether an address is this machine: the loopback network, or the
@@ -301,8 +386,13 @@ pub struct Socket {
transport: Mutex<Option<Arc<dyn Transport>>>,
/// Where `connect` pointed, for `getpeername`.
peer: Mutex<Option<(u32, u16)>>,
/// Datagrams waiting to be read. Only the resolver puts anything here.
dgrams: Mutex<VecDeque<Vec<u8>>>,
/// Datagrams waiting to be read, with where each came from: the
/// resolver's answers, and datagrams from guests on this machine.
dgrams: Mutex<VecDeque<(Vec<u8>, (u32, u16))>>,
/// Where the last datagram read came from, for `recvfrom`.
last_from: Mutex<Option<(u32, u16)>>,
/// This socket, for registering itself (a UDP port's table entry).
me: Mutex<std::sync::Weak<Socket>>,
nonblocking: AtomicBool,
/// The machine's listening ports.
ports: Arc<Ports>,
@@ -312,10 +402,18 @@ pub struct Socket {
/// A listener's connections waiting for `accept`: the far end of each,
/// and the port it came from. `None` unless listening.
backlog: Mutex<Option<VecDeque<(Arc<dyn Transport>, u16)>>>,
/// The global kernel, when there is one (a page: a Worker per process).
/// Listeners and loopback connections live there, so a guest in another
/// process can reach them.
remote: Option<crate::unix::Remote>,
/// A listener's id in the global kernel.
remote_listener: Mutex<Option<i32>>,
/// A UDP socket's streams in the global kernel.
udp_remote: Mutex<UdpRemote>,
}
impl Socket {
pub fn new(kind: Kind, net: Arc<dyn NetStack>, resolver: Arc<Resolver>, ports: Arc<Ports>) -> Socket {
pub fn new(kind: Kind, net: Arc<dyn NetStack>, resolver: Arc<Resolver>, ports: Arc<Ports>, remote: Option<crate::unix::Remote>) -> Socket {
Socket {
kind,
net,
@@ -323,13 +421,66 @@ impl Socket {
transport: Mutex::new(None),
peer: Mutex::new(None),
dgrams: Mutex::new(VecDeque::new()),
last_from: Mutex::new(None),
me: Mutex::new(std::sync::Weak::new()),
nonblocking: AtomicBool::new(false),
ports,
local: Mutex::new(None),
backlog: Mutex::new(None),
remote,
remote_listener: Mutex::new(None),
udp_remote: Mutex::new(UdpRemote::default()),
}
}
/// Remember the `Arc` this socket lives in (made right after `new`).
pub fn set_self(self: &Arc<Self>) {
*self.me.lock().unwrap() = Arc::downgrade(self);
}
/// Where the last datagram read came from.
pub fn last_from(&self) -> Option<(u32, u16)> {
*self.last_from.lock().unwrap()
}
/// Give this UDP socket a port (`bind`, or the first send unbound),
/// registered so guests here can send to it.
fn udp_take(&self, addr: u32, mut port: u16) -> Result<(), Errno> {
// In a page: the port is a name in the global kernel, where every
// process sees it.
if let Some(r) = self.remote {
let fixed = port != 0;
for _ in 0..1000 {
if !fixed {
port = self.ports.udp_ephemeral();
}
let n = udp_name(port);
let id = (r.bind)(n.as_ptr() as crate::PAddr, n.len() as u32);
if id >= 0 {
self.udp_remote.lock().unwrap().listener = Some(id);
*self.local.lock().unwrap() = Some((addr, port));
return Ok(());
}
if fixed || -id != Errno::EADDRINUSE.0 {
return Err(Errno(-id));
}
}
return Err(Errno::EADDRINUSE);
}
let mut udp = self.ports.udp.lock().unwrap();
if port == 0 {
drop(udp);
port = self.ports.udp_ephemeral();
udp = self.ports.udp.lock().unwrap();
}
if udp.get(&port).is_some_and(|w| w.strong_count() > 0) {
return Err(Errno::EADDRINUSE);
}
udp.insert(port, self.me.lock().unwrap().clone());
*self.local.lock().unwrap() = Some((addr, port));
Ok(())
}
/// `bind`: where this socket is. A port another listener holds is
/// `EADDRINUSE` (SO_REUSEADDR is not modelled: a listener that went away
/// frees its port at once).
@@ -337,13 +488,26 @@ impl Socket {
if self.local.lock().unwrap().is_some() {
return Err(Errno::EINVAL);
}
if port != 0 && self.kind == Kind::Tcp && self.ports.listening(port) {
if self.kind == Kind::Udp {
return self.udp_take(addr, port);
}
// (In a page the global kernel answers at `listen`: its name table
// keeps a closed listener's name until the name is bound again.)
if port != 0 && self.kind == Kind::Tcp && self.remote.is_none() && self.ports.listening(port) {
return Err(Errno::EADDRINUSE);
}
*self.local.lock().unwrap() = Some((addr, port));
Ok(())
}
/// Whether a guest in any process of the page listens on `port`.
fn remote_taken(&self, port: u16) -> bool {
self.remote.is_some_and(|r| {
let n = tcp_name(port);
(r.exists)(n.as_ptr() as crate::PAddr, n.len() as u32) > 0
})
}
/// Where this end is, for `getsockname`: 0.0.0.0:0 when unbound.
pub fn local(&self) -> (u32, u16) {
self.local.lock().unwrap().unwrap_or((0, 0))
@@ -360,6 +524,25 @@ impl Socket {
return Ok(());
}
let (addr, mut port) = self.local();
if let Some(r) = self.remote {
// In the global kernel: bound and listening in one, as a unix
// socket there is.
if port == 0 {
port = self.ports.ephemeral();
while self.remote_taken(port) {
port = self.ports.ephemeral();
}
}
let n = tcp_name(port);
let id = (r.bind)(n.as_ptr() as crate::PAddr, n.len() as u32);
if id < 0 {
return Err(if -id == Errno::EADDRINUSE.0 || -id == 98 { Errno::EADDRINUSE } else { Errno(-id) });
}
*self.remote_listener.lock().unwrap() = Some(id);
*self.local.lock().unwrap() = Some((addr, port));
*backlog = Some(VecDeque::new());
return Ok(());
}
let mut listeners = self.ports.listeners.lock().unwrap();
if port == 0 {
drop(listeners);
@@ -382,15 +565,25 @@ impl Socket {
/// `accept`: the next waiting connection as a socket of its own, or
/// `EAGAIN`; `EINVAL` when not listening.
pub fn accept(&self) -> Result<Arc<Socket>, Errno> {
let (t, from) = match &mut *self.backlog.lock().unwrap() {
Some(q) => q.pop_front().ok_or(Errno::EAGAIN)?,
None => return Err(Errno::EINVAL),
let remote_listener = *self.remote_listener.lock().unwrap();
let (t, from): (Arc<dyn Transport>, u16) = match (remote_listener, self.remote) {
(Some(id), Some(r)) => match (r.accept)(id) {
// The far end's port is the other process's to know.
n if n >= 0 => (Arc::new(RemoteEnd { id: n, remote: r }), 0),
n => return Err(Errno(-n)),
},
_ => match &mut *self.backlog.lock().unwrap() {
Some(q) => q.pop_front().ok_or(Errno::EAGAIN)?,
None => return Err(Errno::EINVAL),
},
};
let s = Socket::new(Kind::Tcp, Arc::clone(&self.net), Arc::clone(&self.resolver), Arc::clone(&self.ports));
let s = Socket::new(Kind::Tcp, Arc::clone(&self.net), Arc::clone(&self.resolver), Arc::clone(&self.ports), self.remote);
*s.transport.lock().unwrap() = Some(t);
*s.peer.lock().unwrap() = Some((0x7f00_0001, from));
*s.local.lock().unwrap() = Some((0x7f00_0001, self.local().1));
Ok(Arc::new(s))
let s = Arc::new(s);
s.set_self();
Ok(s)
}
/// A connection to a port a guest on this machine listens on: both ends
@@ -439,7 +632,16 @@ impl Socket {
// involved. (One nothing here listens on still goes out: the host
// may have a service on its own loopback.)
if self.kind == Kind::Tcp && is_local(addr) {
if let Some(listener) = self.ports.listener(port) {
if let Some(r) = self.remote {
let n = tcp_name(port);
let id = (r.connect)(n.as_ptr() as crate::PAddr, n.len() as u32);
if id >= 0 {
*self.peer.lock().unwrap() = Some((0x7f00_0001, port));
*self.local.lock().unwrap() = Some((0x7f00_0001, self.ports.ephemeral()));
*self.transport.lock().unwrap() = Some(Arc::new(RemoteEnd { id, remote: r }));
return Ok(());
}
} else if let Some(listener) = self.ports.listener(port) {
return self.connect_local(&listener, port);
}
}
@@ -469,11 +671,52 @@ impl Socket {
}
}
/// A datagram to the resolver: answer it and queue the reply.
/// A datagram: to a guest on this machine bound to the port it is for,
/// else to the resolver, which answers it and queues the reply.
pub fn send_dgram(&self, data: &[u8]) -> SysResult {
let to = self.peer();
if let (Some((_, port)), Some(r)) = (to.filter(|&(a, _)| is_local(a)), self.remote) {
if self.local.lock().unwrap().is_none() {
self.udp_take(0, 0)?;
}
if let Some(id) = self.udp_stream_to(r, port) {
let mut frame = Vec::with_capacity(6 + data.len());
frame.extend_from_slice(&(data.len() as u32).to_le_bytes());
frame.extend_from_slice(&self.local().1.to_le_bytes());
frame.extend_from_slice(data);
let mut at = 0;
// A stream's buffer is far larger than a datagram; a full one
// (a receiver that stopped reading) drops it, as UDP may.
for _ in 0..64 {
match (r.write)(id, frame[at..].as_ptr() as crate::PAddr, (frame.len() - at) as u32) {
n if n > 0 => at += n as usize,
_ => {}
}
if at == frame.len() {
break;
}
}
return Ok(data.len() as u64);
}
// Nothing bound there in any process: lost, as UDP is.
if answer_dns(data, &self.resolver).is_none() {
return Ok(data.len() as u64);
}
}
if let Some((addr, port)) = to.filter(|&(a, _)| is_local(a)) {
if let Some(dest) = self.ports.udp_at(port) {
if self.local.lock().unwrap().is_none() {
self.udp_take(0, 0)?;
}
let from = (0x7f00_0001, self.local().1);
let _ = addr;
dest.dgrams.lock().unwrap().push_back((data.to_vec(), from));
return Ok(data.len() as u64);
}
}
match answer_dns(data, &self.resolver) {
Some(reply) => {
self.dgrams.lock().unwrap().push_back(reply);
self.dgrams.lock().unwrap().push_back((reply, to.unwrap_or((0, 0))));
Ok(data.len() as u64)
}
// Not a question this can answer. Accepting it and never
@@ -483,11 +726,71 @@ impl Socket {
}
}
/// The stream to UDP port `port` in the global kernel, opened once.
fn udp_stream_to(&self, r: crate::unix::Remote, port: u16) -> Option<i32> {
let mut u = self.udp_remote.lock().unwrap();
if let Some(&id) = u.outbound.get(&port) {
return Some(id);
}
let n = udp_name(port);
let id = (r.connect)(n.as_ptr() as crate::PAddr, n.len() as u32);
(id >= 0).then(|| {
u.outbound.insert(port, id);
id
})
}
/// Take what senders in other processes wrote: accept new streams, read
/// each, and queue every whole datagram.
fn udp_pump(&self) {
let Some(r) = self.remote else { return };
let mut u = self.udp_remote.lock().unwrap();
let Some(l) = u.listener else { return };
while (r.pending)(l) != 0 {
let id = (r.accept)(l);
if id < 0 {
break;
}
u.inbound.push((id, Vec::new()));
}
let mut q = self.dgrams.lock().unwrap();
let mut chunk = [0u8; 4096];
let mut gone = Vec::new();
for (id, buf) in u.inbound.iter_mut() {
while (r.readable)(*id) != 0 {
let n = (r.read)(*id, chunk.as_mut_ptr() as crate::PAddr, chunk.len() as u32);
if n == 0 {
// The sender closed (its socket or its process went).
gone.push(*id);
}
if n <= 0 {
break;
}
buf.extend_from_slice(&chunk[..n as usize]);
}
while buf.len() >= 6 {
let len = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]) as usize;
if buf.len() < 6 + len {
break;
}
let from = u16::from_le_bytes([buf[4], buf[5]]);
q.push_back((buf[6..6 + len].to_vec(), (0x7f00_0001, from)));
buf.drain(..6 + len);
}
}
for id in gone {
(r.close)(id);
u.inbound.retain(|(i, _)| *i != id);
}
}
pub fn recv_dgram(&self, buf: &mut [u8]) -> SysResult {
self.udp_pump();
match self.dgrams.lock().unwrap().pop_front() {
Some(d) => {
Some((d, from)) => {
let n = buf.len().min(d.len());
buf[..n].copy_from_slice(&d[..n]);
*self.last_from.lock().unwrap() = Some(from);
Ok(n as u64)
}
None => Err(Errno::EAGAIN),
@@ -559,12 +862,13 @@ impl FileHandle for Socket {
/// to size its buffer.
fn available(&self) -> Option<u64> {
if self.kind == Kind::Udp {
self.udp_pump();
return Some(
self.dgrams
.lock()
.unwrap()
.front()
.map_or(0, |d| d.len() as u64),
.map_or(0, |d| d.0.len() as u64),
);
}
Some(
@@ -578,9 +882,13 @@ impl FileHandle for Socket {
fn poll_readable(&self) -> bool {
if self.kind == Kind::Udp {
self.udp_pump();
return !self.dgrams.lock().unwrap().is_empty();
}
// A listener is readable when a connection waits.
if let (Some(id), Some(r)) = (*self.remote_listener.lock().unwrap(), self.remote) {
return (r.pending)(id) != 0;
}
if let Some(q) = &*self.backlog.lock().unwrap() {
return !q.is_empty();
}
@@ -614,6 +922,26 @@ impl Drop for Socket {
if let Some(t) = self.transport.lock().unwrap().take() {
t.close();
}
if let (Some(id), Some(r)) = (self.remote_listener.lock().unwrap().take(), self.remote) {
(r.close)(id);
}
// A UDP socket's streams in the global kernel close (its name goes
// with the listener).
if let Some(r) = self.remote {
let u = std::mem::take(&mut *self.udp_remote.lock().unwrap());
for id in u.listener.into_iter().chain(u.inbound.into_iter().map(|(i, _)| i)).chain(u.outbound.into_values()) {
(r.close)(id);
}
}
// A UDP socket gives its port back.
if self.kind == Kind::Udp {
if let Some((_, port)) = *self.local.lock().unwrap() {
let mut udp = self.ports.udp.lock().unwrap();
if udp.get(&port).is_some_and(|w| w.strong_count() == 0) {
udp.remove(&port);
}
}
}
// A listener gives its port back, and the connections nobody
// accepted see their peer gone.
if let Some(q) = self.backlog.lock().unwrap().take() {
+88 -8
View File
@@ -46,6 +46,7 @@ pub mod nr {
pub const FACCESSAT: u64 = 48;
pub const CHDIR: u64 = 49;
pub const FCHDIR: u64 = 50;
pub const UNSHARE: u64 = 97;
pub const FCHMOD: u64 = 52;
pub const FCHMODAT: u64 = 53;
pub const OPENAT: u64 = 56;
@@ -634,6 +635,10 @@ pub struct Kernel {
pub pin_landed: Vec<(String, u64, u64)>,
/// File mappings whose writes are meant to reach the file.
pub shared_maps: Vec<SharedMap>,
/// The working directories of threads that left the process's
/// (`unshare(CLONE_FS)`), by context address: swapped into `vfs` for
/// the length of each of that thread's system calls.
pub thread_cwd: std::collections::HashMap<u64, String>,
/// Executable mappings made since the host last looked.
pub new_code: Vec<CodeMap>,
/// Executable ranges *unmapped* since the host last looked.
@@ -1258,6 +1263,7 @@ impl Kernel {
sigreturn_legacy: false,
pin_landed: Vec::new(),
shared_maps: Vec::new(),
thread_cwd: std::collections::HashMap::new(),
new_code: Vec::new(),
retired_code: Vec::new(),
next_tmpfile: 0,
@@ -1490,6 +1496,7 @@ impl Kernel {
sigreturn_legacy: false,
pin_landed: Vec::new(),
shared_maps: Vec::new(),
thread_cwd: std::collections::HashMap::new(),
new_code: Vec::new(),
retired_code: Vec::new(),
next_tmpfile: self.next_tmpfile,
@@ -1608,6 +1615,23 @@ impl Kernel {
/// Handle one system call (`svc #0`, or `syscall`).
pub fn syscall(&mut self, m: &mut GuestMem<'_>, pc: u64) -> SyscallOutcome {
// A thread with a working directory of its own (`unshare(CLONE_FS)`)
// has it in `vfs` while its call runs.
if self.thread_cwd.is_empty() {
return self.syscall_inner(m, pc);
}
let me = self.ctx_addr;
let Some(mut cwd) = self.thread_cwd.remove(&me) else {
return self.syscall_inner(m, pc);
};
std::mem::swap(&mut self.vfs.cwd, &mut cwd);
let out = self.syscall_inner(m, pc);
std::mem::swap(&mut self.vfs.cwd, &mut cwd);
self.thread_cwd.insert(me, cwd);
out
}
fn syscall_inner(&mut self, m: &mut GuestMem<'_>, pc: u64) -> SyscallOutcome {
self.syscall_pc = pc;
let raw = m
.read_u64(self.ctx_addr + ctx::SYSCALL_NR as u64)
@@ -2164,6 +2188,19 @@ impl Kernel {
self.retry_transfer = Some((n, a));
Some(BlockOp::AcceptWait { socket: Arc::clone(&file.handle) })
}
// `recvfrom` (and `recv`) on an empty blocking socket waits for
// it to be readable, then reads under the lock, as `recvmsg`
// does (by `retry_transfer`). It had answered EAGAIN.
nr::RECVFROM => {
let file = self.fds.get(a[0] as i32).ok()?;
if a[3] & Self::MSG_DONTWAIT != 0 || !file.would_block() {
return None;
}
self.retry_transfer = Some((n, a));
Some(BlockOp::AcceptWait {
socket: Arc::clone(&file.handle),
})
}
nr::RECVMSG => {
let file = self.fds.get(a[0] as i32).ok()?;
// A non-blocking socket, one with data, or a call that asked
@@ -2455,6 +2492,19 @@ impl Kernel {
nr::GETCWD => self.sys_getcwd(m, a[0], a[1]),
nr::CHDIR => self.sys_chdir(m, a[0]),
nr::FCHDIR => self.sys_fchdir(a[0] as i32),
// Only a thread's own working directory (CLONE_FS) is unshared;
// a private descriptor table or namespace is not supported.
nr::UNSHARE => {
if a[0] & !0x200 != 0 {
Err(Errno::EINVAL)
} else {
if a[0] & 0x200 != 0 {
let cwd = self.vfs.cwd.clone();
self.thread_cwd.entry(self.ctx_addr).or_insert(cwd);
}
Ok(0)
}
}
nr::MKDIRAT => self.sys_mkdirat(m, a[0] as i32, a[1], a[2] as u32),
nr::UNLINKAT => self.sys_unlinkat(m, a[0] as i32, a[1], a[2]),
nr::RENAMEAT => self.sys_renameat(m, a[0] as i32, a[1], a[2] as i32, a[3]),
@@ -2511,7 +2561,9 @@ impl Kernel {
// unmapping does: `ld.so` applies RELRO by dropping write,
// but a JIT in the guest flips a page from executable back to
// writable precisely so it can put different code there.
if (a[2] as u32) & crate::mem::prot::EXEC == 0 {
// Making code writable is the other half (a patch, a
// detour), which x86 needs no cache flush to see.
if (a[2] as u32) & crate::mem::prot::EXEC == 0 || (a[2] as u32) & crate::mem::prot::WRITE != 0 {
self.retire_code(a[0], a[1]);
}
self.mem.protect(a[0], a[1], (a[2] & 0x7) as u32).map(|_| 0)
@@ -3271,6 +3323,9 @@ impl Kernel {
fn transfer_syscall(&mut self, m: &mut GuestMem<'_>, n: u64, a: &[u64; 6]) -> SysResult {
match n {
nr::SENDFILE => self.sys_sendfile(m, a[0] as i32, a[1] as i32, a[2], a[3]),
// A blocking `recvfrom`/`recv` that waited for a datagram or
// bytes (the classifier below).
nr::RECVFROM => self.sys_recvfrom(m, a[0] as i32, a[1], a[2], a[4], a[5]),
nr::COPY_FILE_RANGE => self.sys_copy_file_range(m, a[0] as i32, a[1], a[2] as i32, a[3], a[4], a[5] as u32),
_ => self.sys_splice(m, a[0] as i32, a[1], a[2] as i32, a[3], a[4]),
}
@@ -4687,8 +4742,16 @@ impl Kernel {
if from >= to {
continue;
}
let bytes = m.read(from, (to - from) as usize)?;
map.file.pwrite(map.offset + (from - map.start), bytes)?;
// Only what the file holds: a shared mapping's page past the
// end of the file is not the file's (Linux never extends it).
let at = map.offset + (from - map.start);
let size = map.file.metadata().map(|md| md.size).unwrap_or(u64::MAX);
let n = (to - from).min(size.saturating_sub(at));
if n == 0 {
continue;
}
let bytes = m.read(from, n as usize)?;
map.file.pwrite(at, bytes)?;
}
Ok(())
}
@@ -5000,6 +5063,17 @@ impl Kernel {
fn sys_mmap(&mut self, m: &mut GuestMem<'_>, a: [u64; 6]) -> SysResult {
let (addr, len, prot_bits, flags, fd, offset) =
(a[0], a[1], a[2] as u32, a[3], a[4] as i32, a[5]);
// A fixed mapping over code replaces it, as an unmap would (Wine's
// VirtualFree decommits by mapping over the range).
if flags & mflags::FIXED != 0 {
self.retire_code(addr, len);
// And a shared file mapping it replaces ends as an unmap ends
// it: written back, and no longer handed to the next mapper.
if self.shared_maps.iter().any(|s| s.start < addr + len && s.start + s.len > addr) {
self.flush_shared(m, addr, len)?;
self.shared_maps.retain(|s| s.start + s.len <= addr || s.start >= addr + len);
}
}
if flags & mflags::ANONYMOUS == 0 && fd >= 0 {
// A private file mapping is an eager copy, which is exactly
// correct and needs no page faults (PLAN §7.4).
@@ -5029,10 +5103,12 @@ impl Kernel {
// see the same pixels, not a zeroed copy. Not reference counted:
// unmapping one alias unmaps both, and nothing here unmaps early.
if flags & crate::mem::flags::SHARED != 0 {
// (or any part of one: a window into a mapping another
// view holds whole, as win32u maps the session's blocks)
if let Some(existing) = self.shared_maps.iter().find(|s| {
Arc::ptr_eq(&s.file, &file) && s.offset == offset && s.len >= len_aligned
Arc::ptr_eq(&s.file, &file) && s.offset <= offset && offset + len_aligned <= s.offset + s.len
}) {
return Ok(existing.start);
return Ok(existing.start + (offset - existing.offset));
}
}
// The path and offset travel with the mapping. Two processes
@@ -5304,7 +5380,8 @@ impl Kernel {
SOCK_DGRAM => net::Kind::Udp,
_ => return Err(Errno::EPROTONOSUPPORT),
};
let sock = Arc::new(net::Socket::new(kind, net, Arc::clone(&self.resolver), Arc::clone(&self.ports)));
let sock = Arc::new(net::Socket::new(kind, net, Arc::clone(&self.resolver), Arc::clone(&self.ports), self.unix_remote));
sock.set_self();
sock.set_nonblocking(ty & SOCK_NONBLOCK != 0);
let flags = if ty & SOCK_NONBLOCK != 0 {
crate::fs::oflags::O_NONBLOCK
@@ -5518,7 +5595,7 @@ impl Kernel {
/// Only ranges that overlap an executable mapping are worth reporting:
/// unmapping the heap or a data file happens constantly and has no
/// translations behind it.
fn retire_code(&mut self, addr: u64, len: u64) {
pub fn retire_code(&mut self, addr: u64, len: u64) {
let (start, end) = (
crate::mem::page_align_down(addr),
crate::mem::page_align_up(addr.saturating_add(len)),
@@ -5883,7 +5960,9 @@ impl Kernel {
let n = self.fds.get(fd)?.read(&mut tmp)?;
m.write(buf, &tmp[..n as usize])?;
if addr != 0 {
let peer = self.socket_of(fd).ok().and_then(|s| s.peer());
// Where the datagram came from (a guest's port, the resolver),
// else the connected peer.
let peer = self.socket_of(fd).ok().and_then(|s| s.last_from().or_else(|| s.peer()));
let (ip, port) = peer.unwrap_or((0, 0));
self.sys_write_sockaddr(m, ip, port, addr, addrlen)?;
}
@@ -6681,6 +6760,7 @@ pub fn syscall_name(n: u64) -> &'static str {
nr::FACCESSAT | nr::FACCESSAT2 => "faccessat",
nr::CHDIR => "chdir",
nr::FCHDIR => "fchdir",
nr::UNSHARE => "unshare",
nr::FCHMOD => "fchmod",
nr::FCHMODAT => "fchmodat",
nr::OPENAT => "openat",
+7 -1
View File
@@ -473,6 +473,12 @@ impl SocketNames {
self.bound.lock().unwrap().retain(|(p, _)| p != path);
}
/// Unbind `path` if it is `sock`'s (a socket closing gives up an abstract
/// name, not one something else bound since).
pub fn unbind_socket(&self, path: &str, sock: &Arc<UnixSocket>) {
self.bound.lock().unwrap().retain(|(p, s)| !(p == path && Arc::ptr_eq(s, sock)));
}
/// Connect `client` to whatever is listening on `path`.
pub fn connect(&self, client: &UnixSocket, path: &str) -> SysResult {
if let Some(listener) = self
@@ -1069,7 +1075,7 @@ mod epoll_tests {
/// own memory) per process, the two ends of a socket sit in two memories where
/// the `Arc<Pipe>` above means nothing, so the socket lives in the shared
/// kernel and each end is just an id; only the id and the bytes cross.
#[derive(Clone, Copy)]
#[derive(Clone, Copy, Debug)]
pub struct Remote {
/// Bind and listen in one. Returns a listener id or a negated errno.
pub bind: extern "C" fn(path: crate::PAddr, path_len: u32) -> i32,
+2
View File
@@ -248,6 +248,7 @@ pub mod raw {
pub const PSELECT6: u64 = 270;
pub const PPOLL: u64 = 271;
pub const SET_ROBUST_LIST: u64 = 273;
pub const UNSHARE: u64 = 272;
pub const GET_ROBUST_LIST: u64 = 274;
pub const UTIMENSAT: u64 = 280;
pub const EPOLL_PWAIT: u64 = 281;
@@ -555,6 +556,7 @@ pub fn translate(
x::PSELECT6 => nr::PSELECT6,
x::PPOLL => nr::PPOLL,
x::SET_ROBUST_LIST => nr::SET_ROBUST_LIST,
x::UNSHARE => nr::UNSHARE,
x::GET_ROBUST_LIST => nr::GET_ROBUST_LIST,
x::UTIMENSAT => nr::UTIMENSAT,
x::EPOLL_PWAIT => nr::EPOLL_PWAIT,
+545 -24
View File
@@ -170,21 +170,45 @@ The plan does the same with **wasm32 as Wine's host architecture**:
- **Native Unix libraries,** in priority order: ntdll.so, win32u.so,
winewayland.drv, opengl32, ws2_32, crypt32, secur32, dnsapi, nsiproxy,
winepulse/winealsa (audio, §6), then winevulkan.
- **wineserver in-process (decided after W0(a), 2026-10-04).** wineserver
shares coherent `MAP_SHARED` memory with its clients: session objects,
- **wineserver in-process, as a thread (chosen design; built in W3.1,
approved 2026-10-06).** wineserver shares coherent `MAP_SHARED` memory
with its clients: the user shared data (the clock), session objects,
and since Wine 9 the queue and desktop state win32u reads directly. gex
processes have separate linear memories, and in the browser separate
`WebAssembly.Memory`s cannot alias. So wineserver runs as a thread of the
Windows process: `clone(CLONE_VM)` without `CLONE_FILES`, i.e. the same
memory but its own fd table, its own module instance and its own data
region. Its protocol stays Wine's (the Unix socket, SCM_RIGHTS, the
request and reply pipes), so the server and ntdll.so are unmodified Wine.
- It needs gex-kernel to support a long-lived `CLONE_VM` process (shared
memory, separate fd table and pid).
- Several Windows processes in one session share one address space.
That fits i386 programs (2 GiB each at most) only with memory64; until
then, one Windows process per session, which covers the games.
- **Later:** a direct-call transport in place of the socket.
`WebAssembly.Memory`s cannot alias, so the server lives in the Windows
process: linked into the Wine loader (the server's objects compiled
with `server/wasm32_inproc.h`, which renames what both sides define and
makes its `main` and exits `wineserver_main`/`wineserver_exit`) and
started by ntdll.so's `start_server` as a **thread**. It has its own
working directory (`unshare(CLONE_FS)`), shares the process's
descriptors and memory, takes no SIGCHLD (the process's children are
ntdll.so's), and maps the user shared data at the client's 0x7ffe0000
(one wasm memory cannot map a page at two addresses; gex hands a second
`MAP_SHARED` of the same open file the first mapping's pages, so later
maps, the session's, simply share the server's address). Its protocol
stays Wine's (the Unix socket, SCM_RIGHTS, request and reply pipes): the
server and ntdll.so are otherwise unmodified Wine. No host-side process
machinery is involved, so it is the same in the browser.
- **Known gap: child Windows processes.** A second Windows process
(a launcher starting the game, a crash handler such as Unity's
UnityCrashHandler, an out-of-process RPC/COM server, a process a
program spawns) is a separate gex process: it connects to the first
process's server through the socket and works, but it cannot see the
server's shared memory. It reads a stale copy of what it mapped: the
clock in its user shared data does not move, and win32u's session
state (windows, queues, the desktop) is not shared with it. One
Windows process per session, which covers most games, but not
launcher-started ones.
- **What would lift it** (later, none planned yet): a `MAP_SHARED` that
stays coherent across gex processes. Natively, the host can map one
memfd into several linear memories at the same guest address (the
memories are host mappings). In the browser there is no aliasing
between `WebAssembly.Memory`s; the options are processes of one
session as threads of one memory (memory64 for several 2 GiB i386
address spaces), or copying the shared pages at synchronisation
points (server requests, waits) for the state that tolerates it (the
clock), which does not cover win32u's lock-free reads. A direct-call
transport in place of the socket is a separate, later optimisation.
- **Sync through ntsync.** Wine 11 supports the ntsync driver
(`dlls/ntdll/unix/sync.c`). gex-kernel implements `/dev/ntsync`, and its
ioctls (semaphores, mutexes, events, wait-any/all) go to the kernel's
@@ -823,6 +847,456 @@ Native Wine runs inside the guest thread, as Windows' DLLs do:
Today's path keeps working throughout. Programs switch per process
(`--wine native`) until W6.
### 4.0 W3 plan (2026-10-06, the user's order: W3 before W7)
**Goal:** i386 Windows games on Wine's own Unix side, natively and in the
page, with no NT layer: ntdll.so, wineserver, win32u.so, a display
driver, opengl32 through gex-gl, and a real registry (`system.reg`,
`user.reg`) written by `wineboot`. Today's native path (W2) is Wine's PE
side as native wasm over gex-kernel's NT layer: every NT system call, the
registry (synthesized), windows and hooks (the NT layer's win32u calls)
and GL (the NT layer's command ring) are still the NT layer's, which is
where G's findings come from.
**Acceptance cases** (G runs them on copies of the W prefix and sends
repros):
1. **Retrocycles:** `CoCreateInstance(CLSID_DirectSound)` succeeds (the
class is registered by `wineboot`'s own registration), and GL texture
uploads over 8 MiB work (no command ring: opengl32.so calls gex-gl).
2. **Katana ZERO:** `SetWindowsHookEx` (win32u.so's real hooks), then to
its menu by keyboard.
3. **Undertale** to gameplay, as G's script drives it today.
Plus the standing gates: tests/nt on the new path, W's checks.
**What exists:** the Unix side nearly compiles for wasm32 already (W1's
`make -k`): ntdll/unix fails only on `current_machine` and the CPU-feature
helpers (`system.c`); wineserver ran in W0(a) as a wasm32 program; ws2_32's
unixlib ran through the dlopen registry (§3.12).
**Steps** (agent time):
| step | work | done when | estimate |
|---|---|---|---|
| W3.0 process shape | Decide and spike how a Wine process runs: (A) the wasm32 `wine` loader is the process (a wasmprog) and runs x86 code through a gex `simulate(ctx)` import (Wine's BTCpu contract, as FEX/Box64), x86 faults reaching ntdll.so's handlers as signals to a wasm32 program; or (B) keep today's x86 NT boot and send the PE side's system calls to ntdll.so linked into the process, its Linux calls through the wasm32 ABI. A retires the NT boot itself; B reuses more of W2. Expected: A. | hello.exe's first NT call reaches ntdll.so on the chosen shape | 1–2 d |
| W3.1 ntdll.so | `current_machine`, CPU features, `signal_wasm32.c` (Unix side: the syscall dispatcher as a direct call, contexts from gex's, user callbacks nested, APCs, exceptions from gex faults), `virtual.c` on gex-kernel's mmap (PE images mapped executable, so gex sees the code), threads (pthread plus a guest context), process start through Wine's loader; wineserver in process (`clone(CLONE_VM)` without `CLONE_FILES`, §3.5) | tests/nt hello, sync, tls, seh, apc, cxx on the new path (`--wine unix`) | 5–7 d |
| W3.2 prefix and registry | `wineboot --init` natively: setupapi, rundll32, regsvr32 and the builtin DLLs' self-registration, writing `system.reg`/`user.reg` through wineserver | Retrocycles' dsound class registered; a prefix made from nothing | 1–2 d |
| W3.3 windows and input | win32u.so plus a display driver: winewayland.drv to gex's compositor (libwayland-client and libxkbcommon built for wasm32), or a small gex driver if that is cheaper | tests/nt's GUI tests; Katana ZERO's hooks and menu | 4–6 d |
| W3.4 GL | opengl32.so through winewayland's EGL to gex-gl (natively the host GL, in the page Mesa wink); wined3d native | Retrocycles' uploads over 8 MiB; Undertale's GL | 2–3 d |
| W3.5 audio, sockets, crypto | a gex audio driver behind winmm/dsound (Web Audio in the page; first a null device so games start), ws2_32 and crypt32/secur32 unixlibs | Undertale to gameplay with sound | 2–3 d |
| W3.6 sync | ntsync in gex-kernel (`/dev/ntsync` on the futex machinery), so waits never reach wineserver | job-heavy games (Noita) no slower than W2 | 2–3 d |
| W3.7 page | the same in the page: wineserver's thread in a Worker, the Unix side's modules per Worker | Retrocycles, Katana ZERO and Undertale in the page | 3–5 d |
**W3.1 progress (2026-10-06, merge/w3 + fork `wasm32-w3`):**
- **Fault delivery (done natively):** `gex.fault_hook(idx)`; a memory
access past the guest memory under a call-out, an illegal instruction,
int3, a divide error or a #GP in the program's code calls ntdll.so's
`__wine_gex_fault(ctx, code, addr)` with NTSTATUS codes, which sets up
KiUserExceptionDispatcher as i386's signal handlers do; the call-out
resumes there with the native frames kept (`spike-w3/fault.c`, all four
kinds). PC for a memory fault is the block's start.
- **The dispatcher:** `ntdll/unix/signal_wasm32.c` (i386's frame, context
and dispatcher-stack code, with gex in place of signal contexts and
assembly): PE system calls arrive from gexrt (`__gexw_syscall`, which
asks `gex.unix_dispatcher`) in the i386 stub's stack shape, run the
service by table and argument bytes, and write the frame back; user
callbacks run KiUserCallbackDispatcher under `gex.call_out` until
NtCallbackReturn; threads start at LdrInitializeThunk (i386's
init_syscall_frame). Unix calls go straight through (one memory and
function table).
- **The loader runs:** Wine's `loader/main.c` and ntdll's Unix side link as
one wasm32 program (`$W/link-w3.sh` → `$W/wine-w32-w3/dlls/ntdll/
wine-w3`, container flag `--gex-guest-i386`: i386 translation in the
process, no mapping above 0x7ffe0000). `wine --version` works; a run
starts wineserver (a wasm32 program, spawned) and talks to it.
- **hello_args.exe runs on Wine's own Unix side** (2026-10-06): PE ntdll,
kernel32, kernelbase and ucrtbase are the native wasm skeletons, bound by
`gex.image_mapped` when ntdll.so maps them (Wine's virtual.c tells gex
each image it maps); exit 42 and the expected output. What it took: the
install layout (`$W/w3dist`: `lib/wine/i386-unix/wine`,
`lib/wine/i386-windows` with the skeletons plus every other i386 builtin
of the master prefix as symlinks, since ntdll.so reads apisetschema.dll
there; `share/wine/nls`; `bin/wineserver`), the loader as its own
`wineloader`, and each thread's kernel stack top-down (gex's dispatch
directory takes 0x100000-0x300000, so bottom-up it landed on 0x400000,
the exe mapped elsewhere, and the loader fell back to start.exe).
Run: `gexc run --env WINEPREFIX=$W/pfx-w3 $W/w3dist/lib/wine/i386-unix/wine
X.exe` (`$W/gexc-w3` wraps that for tests/nt/run.sh; `GEX_W3_TRACE=1` in
gexc's own environment lists the bindings). wineboot.exe is not in the
master prefix, so every start warns "failed to start wineboot" (W3.2).
- **tests/nt i386 on this path** (`$W/nt-run-x86.sh $W/gexc-w3`, log
`$W/w3-nt2.log`; the pe-x64 cases in that run are W7's): **24 of 40**
(2026-10-06, up from 20): hello_args, sync, mtx, crtmem, lowmem, apc,
tls, cxx, icall, fixedbase(-taken), loadorder-native/-disabled, zfcall,
window, pagespan, exitproc, seh, seh-traps, hello32, msvc, msvc_cxxeh,
msvc_x87, msvc_largeint. G's repros: unixpath and iocp pass; sysmisc has
only the two SPI_GET checks left (W3.3); hooks and keyname are W3.3.
What it took, beyond the first run:
- **wineserver in process (§3.5, done):** linked into the loader and
started by `start_server` as a thread (fork: `server/wasm32_inproc.h`,
force-included into the server objects by `$W/link-w3.sh`, renames
the eight symbols both sides define and makes the server's `main` and
exits `wineserver_main`/`wineserver_exit`; the thread ends instead of
the process). It has its own working directory through
`unshare(CLONE_FS)` (new in gex-kernel: a thread's cwd swapped into
the VFS for its calls), shares the descriptors, takes no SIGCHLD (its
waitpid(-1) stole ntdll.so's children: a third of the starts hung),
and maps the user shared data at 0x7ffe0000, the client's address (a
wasm memory cannot map one page twice; gex hands a second `MAP_SHARED`
of the same file the first one's pages). The tick count now moves.
The nls directory is found beside the loader
(`lib/wine/share -> ../../share`, in `$W/stage-w3.sh`).
- **exceptions:** the fault hook's C frames go below the guest stack
pointer with a red zone (the shared shadow stack pointer could sit
above the live guest stack and the hook overwrote the SEH chain it
walked); an int3's address is its own; #GP at a privileged
instruction is EXCEPTION_PRIV_INSTRUCTION (i386's
`is_privileged_instr`); a branch to memory that is not executable is
an access violation with kind 8 (the hook got a fourth argument);
gexrt's NATIVE_LEFT is taken without the NT layer (a C++ catch through
wasm msvcrt nested a call-out per throw until the limit); the NT
layer's trampoline page (jmp eax, the vtable wrappers) is mapped at
0x3f0000 in a W3 process.
- a new thread's start function at pthread's type; i386 mlock as an
advisory no-op.
**Then 29 of 40** (`$W/w3-nt4.log`, same day): suspend, msvc_msvcp,
detour, jitcode, exitproc and loadorder-builtin too, from:
- **signals:** SIGUSR1 (suspend), SIGQUIT (thread abort) and SIGINT
handlers as i386's. One arriving in guest code is taken at a gex
safepoint: gex-kernel builds i386's frame on the guest stack and gex
runs it through `gex.signal_hook` (libgex's frame runner; the handler
gets the frame's ucontext, the guest's state). One arriving in Unix
code is taken when a system call returns, with the syscall frame's
state. Threads unblock the server's signals before entering the
program.
- **the Unix side's stack and registers:** system calls from the PE
side run on the thread's own stack below its syscall frame (an
assembly switch), not on the guest stack just above the arguments
where callback and APC frames are written; a Unix call puts the
program's registers back after (every system call the Unix side makes
passes arguments in EBX..EBP of the same context: msvcp140 lost a
return address to it).
- **code changes:** a fixed mmap over code retires it (Wine decommits
by mapping over), making code writable retires it, and
`gex.code_changed` serves NtFlushInstructionCache.
- **thread exits:** the first thread's `exit` while others run no
longer ends the process (Linux semantics; ExitProcess on another
thread lost its exit code).
- **file mappings:** a shared mapping replaced by a fixed mmap ends as an
unmap (written back, no longer aliased), and write-back stops at the
file's end.
- **Unix libraries:** crypt32, ws2_32, msv1_0, dnsapi and nsiproxy's
Unix sides are linked in (`$W/link-w3.sh`, renamed apart, dlopen
registry, §3.12); i386 code reaches them through the trampoline page's
Unix-call entry, which gex hands to ntdll.so (`gex.unix_dispatcher` op
2). For now wasm callers take the same route (a guest round trip per
Unix call); a direct path for them needs a winepe rebuild.
Open, by cause:
- **the registry (W3.2):** there is none until wineboot runs, and
crypt32 then spends minutes failing provider lookups (crypt). The
wasm wineboot.exe now links (programs in the one module: winegcc,
winebuild's program entry, the CRT table markers; stage-native stages
a program) and starts, but **a native module's dependencies are not
loaded**: skeletons have no import table, so the loader never loads
and initialises what the module calls (ucrtbase's heap is 0 in
wineboot), and calls to i386 DLLs (`owner!name` left by pelink) have
nothing to bind them without the NT layer. That is W3.2's first item:
skeletons that carry their module's imports (or gex loading them at
install), then wineboot, then the real system.reg.
- lownull-* (§5.0, low-page protection; not to be fixed here);
- filemap: change notifications (FindFirstChangeNotification) need
inotify in gex-kernel and Wine built with it;
- pipe: SystemSupportedProcessorArchitectures says i386, as 32-bit
Windows does; the test (and the NT layer) expects WoW64 on x86-64;
- win32u's Unix side (W3.3): user_win, gdi, input, display, gl*.
**W3.2 progress (2026-10-06, native DLL dependencies, wineboot):**
- **A native module's imports are loaded:** pelink writes each module's
load-time imports (Makefile IMPORTS plus makedep's default C runtime,
kernel32, ntdll) into `__gexw_module_imports`, and Wine's PE loader on
wasm32 loads those before a skeleton without an import table, as an
import table's would be (fork: ntdll/loader.c fixup_imports).
- **Programs in the one module:** winegcc leaves a program's r.o and
export list, winebuild's entry adapter calls `mainCRTStartup(void)`,
gexrt defines the CRT table markers (`__xi_a`..), and crt_main calls
clang's `__main_argc_argv`/`__original_main` (a 3-parameter `main`
call trapped). `$W/relink-prog-w3.sh` relinks a program with the W3 host
tools (`$W/wine-tools-w3`); `W3_PROGRAMS="wineboot.exe services.exe
plugplay.exe conhost.exe winedevice.exe" $W/stage-w3.sh` stages them.
- **Child processes by clone:** fork() is asyncify's unwind and rewind of
the whole wasm stack, which cannot cross gex's frames and the PE module
under a system call (the forking process went on half rewound, its next
indirect call trapping). spawn_process, fork_and_exec and
`__wine_unix_spawnvp` start children with `clone(CLONE_VFORK)` and exec.
- **Unix calls:** wasm callers go straight to the native dispatcher
(gexrt recognises the trampoline address; `gex.unix_dispatcher` op 4/5),
i386 callers through the trampoline; stateful installs (the program,
the PE module) are providers, so relocating a skeleton or an mprotect
over it retires nothing.
- **Threads:** RtlCreateUserThread wraps a native void entry (the
threadpool's), since every entry is called as LPTHREAD_START_ROUTINE.
- **Registry:** the in-process wineserver saves the registry when the
process exits (exit_process asks it to flush; its periodic save never
came). `system.reg` and `user.reg` are written.
- **pelink:** i386 owners from a .spec when a DLL's wasm32 build failed
(rpcrt4: wineboot's UuidCreate was an undefined, trapping function);
guest-import stubs get entry thunks (stdcall by the owner's .spec), as a
native module may hand one to i386 code (services' RPC dispatch table
holds rpcrt4's NdrServerCall2); the ICALL pass's annotation sections
are stripped before the final link (wasm-ld 22 crashed on services.exe
and plugplay.exe).
- **Entry thunks where a function is handed out but not defined:** widl's
server stubs (svcctl_s.c) list routines another object defines, so
i386 rpcrt4 called table indices with no thunk. The ICALL pass now
makes a thunk for an escaping declaration too (`gexw_entries_decl`,
bound only where no defining object gave one); the plugin is built into
`$W/sr-w3` (a hard-linked copy of sr-slim with the new GexIcall.so).
wmainCRTStartup calls `wmain(argc, argv)` on wasm32 (winedevice).
- **Where it stands:** wineboot runs services.exe (RPC to it works),
winedevice.exe and plugplay.exe natively, and rundll32's wine.inf
`PreInstall` completes (services installed, registry written and saved
at exit). Then wineboot opens its "updating" dialog
(`show_wait_window`: CreateDialog) before `DefaultInstall`, which needs
win32u's Unix side: it does not come back, so the full install (and the
registry it writes) waits for **W3.3**, and every start still waits out
the boot event. The suite's prefix stays without the native programs
until then (`$W/w3-nt5.log`: 29 of 40, no change).
**W3.3 progress (2026-10-06, win32u.so):**
- win32u.so's Unix side is linked into the loader (`$W/link-w3.sh`:
the twelve names it shares with wineserver and ntdll.so renamed apart,
`__wine_unix_lib_init` in the dlopen registry); win32u's DllMain loads
it on wasm32 (its dispatcher check saw ntdll's variable), each native
DLL keeps its own `__gexw_image_base` (pesym had shared one, never
written: unix-call init asked for module 0). NtUser*/NtGdi* calls now
reach win32u.so.
- The loader references no fork any more (the in-process server never
daemonizes), so it is not asyncify-instrumented: smaller, and nothing
can be left half rewound. A call-out's return is never watched in the
miss slots (gex: the trampoline page's pad, translated first).
- **The session's shared memory, resolved with option (a)** (the user's
choice, 2026-10-06): win32u (wasm32) reads the session's blocks from the
in-process server's mapping (`wineserver_session_data`, one 16 MiB
block), and explorer's desktop runs as a thread of the application
(`gexp_desktop_thread`, which win32u's `get_desktop_window` starts once
per process). Both are in the inventory below.
- **The desktop thread, made right:** it creates the desktop window
itself, as explorer does (it owns its WND: IsChild, the window tree and
messages to the desktop work). The thread is created suspended so the
starter knows it: its own `get_desktop_window` before the window exists
returns at once (it had waited out 10 s, after which the application
forced a desktop window of its own). Callers wait until explorer names
the shell window, just before its message loop (a WM_NULL was answered
in explorer's sent-message waits during set-up, before the desktop had
its size). explorer's `ClipCursor`/`SetCursor` imports were trapping
stubs: winebuild's export list now names a renamed `-import` export as a
forward (user32's ClipCursor is win32u's NtUserClipCursor).
- **FreeType for wasm32** (pinned: **FreeType 2.13.3**,
`freetype-2.13.3.tar.xz`, sha256
`0550350666d427c74daeb85d5ac7bb353acba5f76956395995311a9c6f063289`,
fetched from the SourceForge mirror, savannah being unreachable from
gooner): a static build with no zlib/png/bzip2/harfbuzz/brotli into a
directory of its own, `$W/ft/install` (`$W/ft/build-ft.sh`; not in the
sysroot, which shares files with sr-slim). `$W/link-w3.sh` compiles
win32u with `HAVE_FT2BUILD_H` and `SONAME_LIBFREETYPE` (no reconfigure),
links `libfreetype.a` and registers the 31 `FT_*` functions win32u
dlsym's under `libfreetype.so.6` in the dlopen registry. Wine's own
fonts (`fonts/*.ttf`) are staged in the data dir's `fonts/`
(`$W/stage-w3.sh`). Tahoma realizes (13 px: height 16, ascent 13).
Before it, no font could be realized and every window's minimum track
size was stack garbage (a 640-wide window came out 4204 wide); a
stopgap fork patch for that was reverted once FreeType was in.
- Left in the i386 suite (`$W/w3-nt9.log`, 32 of 40, FreeType in):
`pipe` fails only its SystemSupportedProcessorArchitectures check,
which wants AMD64 first (the NT layer's x64 host); W3's Wine is
i386-only (§3.9), so that is an expectation to revisit with x64, not a
pipe bug. `crypt`: CryptAcquireContext finds no provider (rsaenh's
registration is wineboot's DefaultInstall, W3.2's open end).
- The test prefix has the null graphics driver as a fixture
(`$W/stage-w3.sh` writes `[Software\\Wine\\Drivers] "Graphics"="null"`)
and stages explorer.exe as a native program (`W3_PROGRAMS=explorer.exe`).
- tests/nt `display`: the desktop takes a new mode's size from a notify
message (Wine's behaviour with explorer in another thread or process),
so the check polls for up to 1 s.
**Display driver: winewayland.drv, not a small gex driver** (spike,
2026-10-06). The two options, side by side:
| | winewayland.drv + libwayland + libxkbcommon | a small gex driver |
|---|---|---|
| work | build three libraries for wasm32 (wayland 1.23.1 and libxkbcommon 1.7.0 fetched to `$W/wl`); libwayland's one libffi use (`ffi_call` for listener dispatch) replaced by a ~100-line wasm32 `ffi_call` outside any fork (every listener argument is an i32 on wasm32, so it dispatches on the count); link winewayland's Unix side like win32u's; keymap data on the guest side. About 2–3 d | a new Wine driver: window surfaces, input, keyboard layout, cursors, display modes, clipboard (winewayland is ~8000 lines for these) plus new gex host calls in both hosts. About 5–8 d |
| fork patches | winewayland is upstream: expected a few `__wasm32__` build tweaks at most; libwayland/xkbcommon/ffi are dependencies, not forks | the whole driver is a fork addition, tracking upstream's driver interface (`user_driver_funcs`) at every rebase |
| browser | the page already runs gex's compositor on the same wire protocol (`shims/browser/src/wl.rs`): nothing new | new host calls in the page and in the native host |
| GL (W3.4) | gex-gl's EGL already presents to a `wl_surface` (`guest/gl/gexgl.c`: `wl_egl_window`, commit on swap): winewayland's EGL path needs nothing new | its own GL presentation path, new on both hosts |
| jank | one more process-wide socket protocol, but the one every other guest uses | a gex-only driver no upstream test covers |
winewayland wins on every axis that lasts: fewer fork patches (the driver
is upstream), no new host interface, and GL presentation that already
exists. The small driver is only cheaper in dependency count.
**winewayland.drv running (2026-10-06):** a Win32 window painted with
GDI (FillRect, TextOut through FreeType) shows on gex's compositor
(`gexc run --wayland /tmp/wayland-0 --wayland-out ...`). What it took:
- Dependencies, static wasm32, in `$W/wl/install` (`$W/wl/build-wl.sh`,
sha256-checked): **wayland 1.23.1** (`864fb2a8399e2d0ec39d56e9d9b753c093775beadc6022ce81f441929a81e5ed`,
sources compiled directly, scanner built for the host), **libxml2
2.12.9** (`59912db536ab56a3996489ea0299768c7bcffe57169f0235e7f962a91f483590`,
for xkbregistry, which winewayland requires), **libxkbcommon 1.7.0**
(`65782f0a10a4b455af9c6baab7040e2f537520caa2ec2092805cdfd36863b247`,
meson 1.5.2 from a pip wheel). libffi is replaced by `$W/wl/ffi`
(`ffi_call` by argument count: every listener argument is an i32 on
wasm32). XKB data is the host's `/usr/share/X11/xkb`.
- The W3 tree is configured with FreeType and Wayland (`$W/configure-w3.sh`:
sonames given for the static libraries; tools are `$W/wine-tools-w3`).
`$W/link-w3.sh` links winewayland's Unix side (`get_region_data`
renamed apart) and the libraries.
- `tools/wine-wasm/stage-native.sh`: a DLL with no .spec (a driver) gets
an empty one, so its skeleton is a DLL whose DllMain runs (it was an
exe skeleton: the driver loaded but never initialised), and a `.drv`
directory keeps its extension.
- gex-wayland: **wp_viewporter** (Wine's driver will not start without
it): viewports accepted, the destination recorded per surface.
- **No Wine fork patch** for the driver itself.
- The suite's prefix keeps the null driver (`$W/pfx-w3`, 32 of 40);
`W3_GRAPHICS=wayland W3_PROGRAMS="explorer.exe winewayland.drv"
$W/stage-w3.sh $W/pfx-w3wl` stages the Wayland one. On it user_win
(34), gdi (37) and input (20) pass, and display (20 of 24 at
`--wayland-size 1280x1024`) fails only where the null driver does.
- **Display modes:** a Wayland client cannot set the output's mode, so
the Wayland prefix sets Wine's own `EmulateModeset=Y` (HKCU
`Software\\Wine\\X11 Driver`, the fixture in `$W/stage-w3.sh`): win32u
offers the standard sizes up to the output's and switches among them
itself. Modes above the output's are not offered, as on a monitor that
size (at the default 640x480 output, 800x600 is not on offer), so the
compositor needs no fake modes.
- **VkKeyScan** returned letters with Shift swapped: winewayland makes
every letter an SGCAPS pair (Caps Lock row, then the normal row) and
win32u's `kbd_tables_wchar_to_vkey` matched the Caps Lock row. One
fork patch (inventory), an upstream bug fix to send.
**W3.4 GL (2026-10-06): opengl32 on gex-gl through winewayland.** The
i386 suite on the Wayland prefix (`--gl --wayland ... --wayland-size
1280x1024`, `$W/w3-nt-wl1.log`) is **34 of 40**: glcompat (22) and glfast
(16) pass; left are lownull x2 (§5.0), filemap and display (shared views),
crypt (no provider registered), pipe (the x64 architecture check).
- gex-gl's guest library (`guest/gl/gexgl.c`) is compiled for wasm32
(`$W/gl/build-gl.sh` → `$W/gl/gexgl.o`) and linked into the loader;
every function it defines is in the dlopen registry as `libEGL.so.1`
(win32u dlsym's two names and takes the rest from eglGetProcAddress,
which looks them up there), and the libwayland functions it finds by
dlopen are registered too. The tree is configured with OpenGL
(`$W/configure-w3.sh`: an `egl.pc` naming gex-gl, the host's Khronos
headers in `$W/gl/include`); `$W/link-w3.sh` links opengl32's Unix side.
`opengl32.dll` is staged native (`W3_PROGRAMS`).
- gex-gl changes (merge/w3): `eglQueryString(EGL_EXTENSIONS)` lists what it
has (Wine requires the client get-all-proc-addresses and platform-base
extensions, and create-context, no-error and no-config on the display);
`eglGetConfigAttrib` answers buffer size, transparency (EGL_NONE),
colour buffer type, conformance and caveat (a zero buffer size left
win32u no format for its internal context: `wglMakeCurrent(0, 0)` and
`wglDeleteContext` failed); `wl_proxy_marshal` is called as the variadic
it is (a fixed-argument call is a type mismatch on wasm32).
- Objects compiled before the ICALL plugin gained declaration thunks
(`$W/sr-w3`, 09:23 that day) lacked them: opengl32's extension table
handed i386 code a bare table index (`wglGetProcAddress("glFenceSync")`,
an execute fault at 0x84F8). Rebuilding every older object fixed it; a
build tree must not mix objects from before and after a plugin change.
- **No Wine fork patch** for GL.
#### Fork-patch inventory (W3, branch `wasm32-w3` over `wasm32`)
Every change is behind `__wasm32__` (or only built for it). What, why, and
what would let it go:
| patch | why | dropped when |
|---|---|---|
| ntdll/unix `signal_wasm32.c` (new file) and `current_machine`/CPUID | the per-CPU port: gex in place of signal contexts and assembly | never (it is the port) |
| ntdll.so's dir is the loader's; no re-exec; `wineloader` is itself | one closed-world program, no preloader | never |
| `gex.image_mapped` from virtual.c | native DLL skeletons bound when ntdll.so maps them | gex learns image maps another way (unlikely) |
| kernel stacks top-down | gex's dispatch directory sits under 0x400000 | gex moves its directory |
| thread entry at pthread's type; SIGCHLD not taken by the server; no fork in the server | wasm traps on a mistyped indirect call; in-process server | never / with the in-process server |
| wineserver as a thread (`server/wasm32_inproc.h`, `start_server`, `wineserver_exit`, `unshare(CLONE_FS)`), USD at 0x7ffe0000, one 16 MiB session block, registry flush at exit | §3.5: shared memory must be one memory | cross-process coherent shared memory (below) |
| faults as i386 reports them; SIGUSR1/QUIT/INT handlers; unblock at thread start | signals and faults through gex hooks | never (port) |
| Unix side on its own stack; Unix calls keep guest registers | wasm32 syscalls use the guest context's registers | never (port) |
| NtFlushInstructionCache → `gex.code_changed` | gex translates code | never |
| Unix calls from i386 code through the trampoline; wasm callers direct (gexrt) | one pointer serves both | never |
| programs in the one module (winegcc r.o, winebuild exe entry, CRT table markers); crt_main/crt_wmain call clang's main | native programs | upstream wasm32 support (never) |
| native modules' imports loaded (ntdll/loader.c `fixup_imports`) | skeletons have no import table | skeletons that carry real import tables (winebuild) |
| child processes by clone, not fork | asyncify fork cannot cross gex's frames | never under W3 |
| RtlCreateUserThread wraps a native void entry | every entry is called as LPTHREAD_START_ROUTINE | never |
| win32u DllMain always loads win32u.so | one module: the dispatcher variable is ntdll's | separate win32u module (no) |
| user callbacks return through gex's pad | gex watches call-out returns by dispatch slot | never (port) |
| **win32u reads the session from the in-process server** (`wineserver_session_data`) | a wasm memory cannot map the server's pages twice | coherent shared memory (below) |
| **explorer's desktop as a thread** (`gexp_desktop_thread`, win32u starts it once, suspended so it knows the thread, which does not wait for itself; callers wait for the shell window; no shell in that thread; returns, no ExitProcess) | a second Windows process cannot see the session (§3.5) | coherent shared memory across gex processes, so explorer can be its own process again |
| winebuild (wasm32 export list): a renamed `-import` export is a forward | pelink resolves a call by the export list (user32's ClipCursor is win32u's NtUserClipCursor) | never (tool side of the port) |
| win32u `kbd_tables_wchar_to_vkey` skips an SGCAPS pair's Caps Lock row | VkKeyScan swapped Shift for letters under winewayland (every letter an SGCAPS pair) | the fix upstream (it is not wasm32-specific): upstream-ready, ungated, in `docs/wine-upstream/0001-win32u-VkKeyScan-skip-SGCAPS-caps-row.patch`, **not submitted** (needs the user's OK) |
**Future option for the last two (and the server's 16 MiB block)**, not
built: a SharedArrayBuffer-backed extra memory (wasm multi-memory) holding
the session objects, which Wine's Unix side reaches only through its
shared-object read/write macros (`memory.copy` between memories). It would
be coherent across processes natively and in the browser (every process
imports the same extra memory), so explorer could run as its own process
and win32u could map the session normally. Decided later, as a gex-wide
question (it touches fork and more than Wine).
**G's repros as acceptance tests** (`scratch/G/wrepro/`, i386 programs
from tests/nt/src on merge/games2; today each stops in the NT layer):
| repro | what it needs | W3 step |
|---|---|---|
| hooks.exe (Katana ZERO, Spelunky 2) | NtUserSetWindowsHookEx and hook calls | W3.3 (win32u.so) |
| iocp.exe (Spelunky 2) | I/O completion ports | W3.1 (ntdll.so + wineserver) |
| unixpath.exe (Dicey Dungeons) | `\??\unix\` paths | W3.1 (ntdll.so's file code) |
| keyname.exe (Spelunky 2) | NtUserGetKeyNameText | W3.3 (win32u.so, the driver's keyboard layout) |
| sysmisc.exe (OneShot) | NtSetThreadExecutionState, SystemParametersInfo 0x5d/0x5e/0x2000/0x2001, VirtualLock | W3.1 (ntdll.so) and W3.3 (win32u.so) |
| com.exe (N++, OneShot, 5D Chess, Retrocycles) | DirectInput8, MMDevice, WIC, dsound classes registered | W3.2 (wineboot's registration) |
| glbig.exe (Undertale, Retrocycles) | GL calls carrying over 8 MiB | W3.4 (opengl32.so, no ring) |
| glcore.exe (5D Chess, Cruelty Squad) | 3.3 core-profile contexts | W3.4 (winewayland's EGL) |
Also seen in G's games (no repro yet): audio drivers loaded by name and
secur32's Unix side failing soft (W3.5); a synchronous NtNotifyChangeKey and
NtAreMappedFilesTheSame (W3.1); NtSetContextThread on another thread
(W3.1, wineserver); NtCreateUserProcess (W3.1, a crash handler's process).
Momodora's missing glGetShaderSource/glGetAttachedShaders and wined3d draw
errors are W3.4.
**Prefixes are copy-on-write** (merge/w3): `stage-native.sh` links only the
large read-only files (DLLs, programs, fonts, NLS) to the staged prefix and
copies every other file, so a run that writes the registry, win.ini or
INetCookies/index.dat writes its own copy. `$W/pfx-*` were fixed in place
(2026-10-06).
**W3.0 findings so far (2026-10-06, branch merge/w3, fork branch
`wasm32-w3`):**
- Every ntdll/unix object compiles for wasm32 (fork 436bf6eda79:
`current_machine` is i386; CPUID is the x86 lifter's sysreg -100 on the
thread's context; XGETBV reports x87 and SSE only).
- **A wasm32 program calls i386 code through the function table**
(`tools/wine-wasm/spike-w3/gc.c`: it maps `add` as x86 and calls it,
`add(40, 2) = 42`), once the process's translator decodes i386: a wasm32
program leaves the class alone today (an x86-64 parent keeps its own), so
the container needs a flag saying its guest code is i386
(`GEX_W3_COMPAT=1` stands in for it on merge/w3).
- **Faults in that x86 code do not reach the program**
(`spike-w3/fault.c`): a guest access past the memory traps the whole wasm
stack, native frames included, and the run ends. Wine's model needs the
fault delivered to ntdll.so's SIGSEGV handler with the i386 state, the
guest resumed where the handler leaves the context, and the native frames
under the guest code kept. That is W3.1's core (a resumable fault for
guest memory accesses in a wasm32 process, delivered synchronously as a
signal; the W2 NT path dispatches only after the trap). Also: an access
to the low guest pages (a NULL dereference) does not fault under gex at
all (the contexts live there), on any path.
- Option A (the wasm32 loader is the process) stands; B is not needed.
**Total:** about 20–31 agent days, against the 5–8 W3 was given before
W2 showed what stays on the NT layer. Order: W3.0 → W3.1 → W3.2 (closes
Retrocycles' dsound) → W3.3 (Katana ZERO) → W3.4 (Retrocycles' GL) → W3.5
(Undertale) → W3.6 → W3.7. Each step lands on its own (`--wine unix` is
opt-in until W6 retires the NT layer).
### 4.1 W0 results
**(a) wineserver and a Unix library on gex-kernel: works natively.**
@@ -1086,6 +1560,22 @@ plus `make -k`.
bytes in system DLLs, self-modifying hooks. They are rare in this set and
handled by rebinding on write (§3.3).
### 5.0 Cross-cutting: low-page protection (open, for a later decision)
A guest access to the lowest guest pages (a NULL dereference, `[0]`,
`[eax+8]` with EAX = 0) does not fault under gex on any path: those pages
hold gex's own state (thread contexts, the dispatch directory), and
translated code reads and writes them like any memory. Linux and Windows
both keep the first 64 KiB unmapped, and programs rely on the fault:
Windows SEH `__try` around a possibly-NULL pointer, runtimes that turn an
access violation into a NullReferenceException (Mono: stream U works around
it with `MONO_DEBUG=explicit-null-checks`; .NET, the JVM), crash handlers.
Found by W3.0's spike (`tools/wine-wasm/spike-w3/fault.c`). Options to
decide between later: move gex's low state above the guest's range and
guard the low pages in the translators' address check (a cost on every
access without a static offset), or a guard only where a static base is
zero. Not fixed for W3.
### 5.1 Queued (user, 2026-10-05): the guest's Unix userland as native wasm
After the current W2 items (user32/gdi32 native, `_vsnprintf_s`,
@@ -1225,23 +1715,54 @@ Heads:
`$W/bb-gate`, `$W/pfx-gui-fork` alone until the coordinator says done.
- **merge/elf-prov** (wt/elf-prov): the development branch (all commits,
several gex-cgen ones).
- **merge/elf-prov-gate** (wt/elf-prov-gate): the gate-ready shape of
elf-prov at 7d447eb8, on wine-slim 10e816c3 plus the stdin fix: ONE
gex-cgen commit (af98737d), then gex-ld, native, browser, gex-kernel TCP,
- **merge/elf-prov-gate2** (wt/elf-prov-gate2, 02a3ab7a): the gate-ready shape of
elf-prov at d7b7f462, on wine-slim 10e816c3 plus the stdin fix: ONE
gex-cgen commit (ceb54180, incl. glibc_math), then gex-ld (incl. 32-bit
dynsyms), native, browser (incl. the scheduler fixes), gex-kernel TCP,
docs. `$W/check-elfprov-gate.sh` runs its pre-gate checks (output
`$W/gchk.out`). Not yet in it (on merge/elf-prov since): 11c3e8b6 32-bit
dynsyms (gex-ld), 5eaed62a glibc_math (gex-cgen: fold into the one
commit), 749f13cd/the idle fix (browser scheduler). Rebuild the gate
branch with them (same path split: `git checkout <dev head> -- <paths>`
per commit), rerun the checks, then merge master once integ-g lands and
send the head.
`$W/gchk2.out`): **all green on 02a3ab7a** (elf-prov 5/5 native and in
the browser, x86-programs 74, nt --no-cgen 81/81 on x86 and pfx-gui,
cgen-prefix 81/81, wasm32 4/4 + busybox both, browser nt 49/49). Gate
order (coordinator): integ-g, U's fjit fix, E's ejected page, then gate2
with F's final head (both re-key). integ-g landed (master c5620656);
**gate2 head 004538a9** (that master merged in) is green again
(`$W/gchk3.out`: the same counts, browser nt 49/49) and was sent to the
coordinator.
- **W3** (the user's order: before W7): plan in §4.0. gex branch
merge/w3 (wt/w3, off merge/elf-prov) with the W3.0 spike; Wine fork
branch `wasm32-w3` (wt/wine-w3; build tree `$W/wine-w32-w3` via
`$W/build-w32-w3.sh [targets]`, sysroot **`$W/sr-w3`** for every W3 build (a hard-linked copy of sr-slim whose only change is the ICALL plugin with declaration thunks; sr-slim stays read only); loader link
`$W/link-w3.sh`, prefix and dll dir `$W/stage-w3.sh`). hello_args.exe
runs on Wine's own Unix side, wineserver in process. **W3.3 in
progress** (§4.0): win32u.so linked in, the session read from the
in-process server, explorer's desktop as a thread (option (a)); test
prefix `W3_PROGRAMS=explorer.exe $W/stage-w3.sh` (null-driver fixture).
tests/nt i386 **32 of 40** (`$W/w3-nt8.log`; user_win and gdi pass; left:
lownull x2 (§5.0), filemap and display (shared views), crypt, pipe,
glcompat/glfast (W3.4)). FreeType 2.13.3 linked in (`$W/ft`); winewayland.drv draws on gex's
compositor (`$W/pfx-w3wl`). Output modes (Wine's EmulateModeset) and VkKeyScan done; W3.4 GL: opengl32 on gex-gl through winewayland, glcompat and glfast pass (34 of 40 on `$W/pfx-w3wl`). Wine fork head 74de38ed1ba (mw).
**gate2 landed** (master 36f55bb6), and w3-gate1 (wp_viewporter, master
1b4bd047, merged into merge/w3). **merge/w3 gate head** sent after
`$W/check-w3-gate.sh` (output `$W/w3chk.out`): crates green, elf-prov 5/5
native and in the browser, x86-programs 82, nt --no-cgen 83/83 on x86 and
pfx-gui, cgen-prefix 83/83, wasm32 4/4 + busybox both, browser nt 49/49,
W3 i386 32 of 40 (null driver) and 34 of 40 (Wayland + GL). The old merge/elf-prov-gate is deleted. To
rebuild after new dev commits: same path split (`git checkout <dev
head> -- <paths>` per commit), rerun the checks, merge master once
integ-g lands, send the head.
- Since the last notes: the browser scheduler no longer reports "idle"
for a wait4/sleep/futex wait (a child in another Worker), and a thread
whose process exited ends at its next syscall (the pause() one);
glibc_math (floor/ceil/trunc/round and float forms, bitwise);
`gex_ld` reads 32-bit dynamic symbols.
- Open: TCP loopback across browser processes (the port table is per
shim instance; needs the global kernel); UDP listeners; libm sin/cos
- After gate2 (dev only, next landing): f9c5a9ec TCP between a page's
processes (listeners/loopback streams in the global kernel; abstract
names freed on close).
- Also dev only: cb1431d0 UDP between guests (natively and within one
page process) and a blocking recvfrom that waits (it answered EAGAIN).
- Open: UDP between a page's processes (the global kernel has only
streams); AF_INET sockets in a page without a network stack
(EAFNOSUPPORT, so loopback TCP needs GEX_NETWORK there); libm sin/cos
(needs glibc's own bits); a paired page run of the providers on edger
(E holds the lock).
@@ -0,0 +1,26 @@
From: velzie <[email protected]>
Subject: [PATCH] win32u: Skip the Caps Lock entry of an SGCAPS pair in VkKeyScanEx.
In a keyboard layout's VK_TO_WCHARS table an entry with the SGCAPS
attribute holds the characters produced while Caps Lock is on, and the
entry following it holds the normal mapping; kbd_tables_vkey_to_wchar
already reads the pair that way. kbd_tables_wchar_to_vkey did not, and
matched the Caps Lock entry first. winewayland builds every letter of an
xkb layout as such a pair (Lock maps letters to their second level), so
VkKeyScanW('A') returned 0x41 and VkKeyScanW('a') 0x141 instead of 0x141
and 0x41. Skip SGCAPS entries when mapping a character back to a key.
---
--- a/dlls/win32u/input.c
+++ b/dlls/win32u/input.c
@@ -503,6 +503,10 @@
{
for (entry = table->pVkToWchars; entry->VirtualKey; entry = NEXT_ENTRY(table, entry))
{
+ /* An SGCAPS entry holds the mapping with Caps Lock on; the entry
+ * following it holds the mapping without, which is what
+ * VkKeyScan reports. */
+ if (entry->Attributes & SGCAPS) continue;
for (mod = 0; mod < table->nModifications; ++mod)
{
if (entry->wch[mod] == WCH_NONE || entry->wch[mod] != wch) continue;
+28 -3
View File
@@ -2639,6 +2639,16 @@ unsigned eglGetConfigAttrib(void *dpy, void *config, int attr, int *value) {
break;
case 0x3040: /* EGL_RENDERABLE_TYPE */ *value = 0x0048; break; /* OpenGL | ES3 */
case 0x3028: /* EGL_CONFIG_ID */ *value = 1; break;
/* The whole pixel, and the attributes whose zero is not "none": Wine's
win32u builds its pixel formats from these, and took a 0 buffer size
for a format no context could be made for (wglMakeCurrent(0, 0)
failed for want of its internal context) and a 0 transparency for an
unknown type. */
case 0x3020: /* EGL_BUFFER_SIZE */ *value = 32; break;
case 0x3034: /* EGL_TRANSPARENT_TYPE */ *value = 0x3038; break; /* EGL_NONE */
case 0x303F: /* EGL_COLOR_BUFFER_TYPE */ *value = 0x308E; break; /* EGL_RGB_BUFFER */
case 0x3042: /* EGL_CONFORMANT */ *value = 0x0048; break;
case 0x3027: /* EGL_CONFIG_CAVEAT */ *value = 0x3038; break; /* EGL_NONE */
default: *value = 0; break;
}
return EGL_TRUE;
@@ -2866,7 +2876,11 @@ static void gexgl_wl_commit(struct gexgl_egl_surface *s) {
if (!s || !s->wl_proxy) return;
void *wayland = dlopen("libwayland-client.so.0", RTLD_LAZY | RTLD_NOLOAD);
if (!wayland) return;
void (*marshal)(void *, unsigned) = dlsym(wayland, "wl_proxy_marshal");
/* Both are variadic, and called as such: a call through a fixed-argument
type works on native ABIs only by luck, and on wasm32 (Wine's
winewayland, W3) it is a type mismatch, since a variadic function
takes its extra arguments as one pointer. */
void (*marshal)(void *, unsigned, ...) = dlsym(wayland, "wl_proxy_marshal");
if (!marshal) return;
/* A frame callback, asked for before the commit that carries it.
@@ -2880,7 +2894,7 @@ static void gexgl_wl_commit(struct gexgl_egl_surface *s) {
*
* The previous one is destroyed rather than left: this happens every
* frame, and a proxy per frame is a leak per frame. */
void *(*marshal_ctor)(void *, unsigned, const void *, void *) =
void *(*marshal_ctor)(void *, unsigned, const void *, ...) =
dlsym(wayland, "wl_proxy_marshal_constructor");
const void *callback_interface = dlsym(wayland, "wl_callback_interface");
void (*proxy_destroy)(void *) = dlsym(wayland, "wl_proxy_destroy");
@@ -3259,7 +3273,18 @@ const char *eglQueryString(void *dpy, int name) {
/* EGL requires "<major>.<minor> <vendor-specific>", and applications do
parse the number off the front. */
case 0x3054: return "1.5 gex"; /* EGL_VERSION */
case 0x3055: return ""; /* EGL_EXTENSIONS */
/* EGL_EXTENSIONS: of EGL_NO_DISPLAY, the client extensions; of a
display, its own. Each one claimed is true here: platform displays
and eglGetProcAddress for every entry point (core ones included) are
this file's; eglCreateContext takes the version and profile
attributes, ignores the no-error one, and needs no config (there is
one). Wine's win32u requires exactly these before it uses EGL. */
case 0x3055:
return dpy ? "EGL_KHR_create_context EGL_KHR_create_context_no_error "
"EGL_KHR_no_config_context"
: "EGL_EXT_client_extensions EGL_EXT_platform_base "
"EGL_KHR_client_get_all_proc_addresses "
"EGL_EXT_platform_wayland EGL_KHR_platform_wayland";
case 0x308D: return "OpenGL OpenGL_ES"; /* EGL_CLIENT_APIS */
default: return "";
}
+8
View File
@@ -2554,6 +2554,14 @@ export class Gex {
// The shim's exports themselves: a wasm-to-wasm call, no JS.
nt_qpc: e.gex_nt_qpc,
provider_alloc_top: e.gex_provider_alloc_top,
// Wine's Unix side (W3.1): its system-call dispatcher and fault
// hook, process-wide in the shim (the page's delivery of faults
// is still to come).
unix_dispatcher: (op, idx) => e.gex_unix_dispatcher?.(op, idx) ?? 0,
fault_hook: (idx) => e.gex_fault_hook_set?.(idx),
signal_hook: (idx) => e.gex_signal_hook_set?.(idx),
code_changed: (addr, len) => e.gex_code_changed?.(addr, len),
wine_pe_slot: (ptr, len) => e.gex_wine_pe_slot?.(ptr, len) ?? 0,
} : {}),
...(memBase && !wasmprog ? this.#providerImports() : {}),
...(wasmprog ? this.#wasmprogImports(() => inst) : wasmdll ? { syscall: this.#nativeSyscall } : {}),
+31
View File
@@ -1638,3 +1638,34 @@ fn on_retire_pages(ranges: &[(u64, u64)]) {
}
}
}
/// Wine's Unix-side system-call dispatcher (W3.1, as the native
/// `crate::unix_dispatcher`): op 0 sets it, op 1 returns it (0 for none).
static UNIX_DISPATCHER: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
#[no_mangle]
pub extern "C" fn gex_unix_dispatcher(op: i32, idx: u32) -> u32 {
if op == 0 {
UNIX_DISPATCHER.store(idx, Ordering::Relaxed);
return 0;
}
UNIX_DISPATCHER.load(Ordering::Relaxed)
}
/// A native wasm32 program's guest fault hook (W3.1). Recorded; the page
/// does not deliver faults to it yet (W3.7).
static FAULT_HOOK: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
#[no_mangle]
pub extern "C" fn gex_fault_hook_set(idx: u32) {
FAULT_HOOK.store(idx, Ordering::Relaxed);
}
/// A native wasm32 program's signal hook (W3.1). Recorded, as the fault
/// hook is; the page does not run signal frames through it yet (W3.7).
static SIGNAL_HOOK: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
#[no_mangle]
pub extern "C" fn gex_signal_hook_set(idx: u32) {
SIGNAL_HOOK.store(idx, Ordering::Relaxed);
}
+3
View File
@@ -7602,6 +7602,9 @@ fn exit_process(h: &mut Host, code: i32) -> i32 {
// reader waits forever for an EOF nobody sends.
h.kernel.fds = gex_kernel::file::FdTable::new();
h.kernel.remote_sockets.clear();
// Internet sockets too: a listener (in the global kernel) is unbound and
// a connection closed when its last holder goes, as on Linux at exit.
h.kernel.sockets.clear();
// And anything the table did not account for: a guest leaving by a trap
// rather than `exit_group` never reaches the line above (see
// `k_pipe_forget`).
+8 -1
View File
@@ -2371,7 +2371,14 @@ pub extern "C" fn k_sock_available(id: i32) -> i32 {
pub extern "C" fn k_sock_close(id: i32) {
let mut t = socks().lock().unwrap_or_else(|e| e.into_inner());
if let Some(slot) = t.get_mut(id as usize) {
*slot = None;
// A listener on an abstract name (a leading NUL) gives the name up
// as it closes, as on Linux; a path name stays until it is unlinked.
// (TCP ports between a page's processes are abstract names.)
if let Some(s) = slot.take() {
if let Some(p) = s.bound_path().filter(|p| p.starts_with('\0')) {
names().unbind_socket(&p, &s);
}
}
}
}
+7 -1
View File
@@ -414,7 +414,13 @@ pub(crate) fn publish_stateful(
let m = s.ftab.modules.len() as u32;
s.ftab.stateful.insert(m, extras);
}
publish_pairs(sh, memory, module, len, pairs, Tier::Unit)
let install = publish_pairs(sh, memory, module, len, pairs, Tier::Unit)?;
// Native code in place of the bytes at its addresses (a wasm32
// program's functions, Wine's PE module behind its skeletons): what the
// guest does to those bytes (relocating a skeleton, mprotect) retires
// nothing, as for an export provider.
sh.lock().unwrap_or_else(|e| e.into_inner()).ftab.providers.insert(install);
Ok(install)
}
/// Bind more addresses to slots of an existing install (`FuncMap::bind`):
+333 -3
View File
@@ -718,7 +718,9 @@ fn run_program_with(
// alone: it is an i386 ELF (and its kernel speaks the i386 ABI, below),
// but an x86-64 shell that runs it goes on translating its own code
// after the wait.
if !wasmprog::is_wasm32(program) {
// ...unless it hosts i386 code itself (Wine's loader, W3: the header's
// FLAG_GUEST_I386), which is then the process's to translate.
if !wasmprog::is_wasm32(program) || wasmprog::flags(program) & wasmprog::FLAG_GUEST_I386 != 0 {
gex_xlate::set_compat(program.compat());
lockstep::set_compat(program.compat());
gex_kernel::set_compat32(program.compat() && gex_abi::X86);
@@ -1162,6 +1164,20 @@ fn run_program_with(
// runs at the entry. After the directory above, which its bridges
// overwrite.
if nt_setup.is_none() {
if wasmprog::flags(program) & wasmprog::FLAG_GUEST_I386 != 0 {
store.data_mut().with_kernel(|k| k.mem.set_mmap_top(0x7ffe_0000));
// The NT layer's trampoline page, which native Wine's gexrt
// sends the guest to (jmp eax after NATIVE_LEFT, the vtable
// wrappers): the same bytes at the same address.
use gex_kernel::nt::{TRAMPOLINES, TRAMPOLINES_END};
let page = gex_kernel::nt::boot::trampoline_page();
if let Some(dst) = memory.bytes().get_mut(TRAMPOLINES as usize..TRAMPOLINES_END as usize) {
dst.copy_from_slice(&page);
}
store.data_mut().with_kernel(|k| {
k.mem.insert(TRAMPOLINES, TRAMPOLINES_END, gex_kernel::mem::prot::READ | gex_kernel::mem::prot::EXEC, "[nt-trampolines]")
});
}
wasmprog::install(&mut store, &memory, program)?;
}
@@ -1244,6 +1260,11 @@ fn run_program_with(
let Stopped::Exited(exit) = stopped else {
unreachable!()
};
// The first thread ended with `exit` while others run: the process goes
// on, as on Linux, until one of them ends it.
if store.data().thread_only {
wait_for_process_end(&spawn);
}
// Another thread may have ended the process (this one then only saw
// `stopping`).
let exit = spawn.ended.lock().unwrap_or_else(|e| e.into_inner()).take().unwrap_or(exit);
@@ -2083,6 +2104,9 @@ fn run_guest_process(cx: Arc<SpawnContext>, thread: ThreadState, entry: u64) ->
profile::harvest(&mut store)?;
match stopped {
Stopped::Exited(exit) => {
if store.data().thread_only {
wait_for_process_end(&cx);
}
stop_other_threads(&cx, false);
Ok(cx.ended.lock().unwrap_or_else(|e| e.into_inner()).take().unwrap_or(exit))
}
@@ -2273,6 +2297,14 @@ fn run_guest_thread(cx: Arc<SpawnContext>, thread: ThreadState) -> Result<Option
}
}
/// Wait, after the first thread's `exit` (a thread exit, others running),
/// until another thread ends the process.
fn wait_for_process_end(cx: &SpawnContext) {
while !cx.stopping.load(std::sync::atomic::Ordering::Acquire) {
std::thread::sleep(std::time::Duration::from_millis(2));
}
}
/// A thread other than the first ended the process with `exit`: keep the
/// first such end and stop every thread.
fn end_process(cx: &SpawnContext, exit: Exit) {
@@ -2977,6 +3009,36 @@ fn register_imports(linker: &mut Linker<HostState>) -> Result<()> {
check_i386_high(&caller, "an NT syscall");
return r;
}
// A Unix call from guest code under Wine's own Unix side (W3).
if pc as u64 & nt::NATIVE_SYSCALL == 0 {
if let Some(memory) = caller.data().memory.clone() {
let ctx_addr = caller.data().thread.ctx_addr;
if read_u64(memory.bytes(), (ctx_addr + ctx::x(0) as u64) as usize) as u32 == gex_kernel::nt::gex_nr::UNIX_CALL
&& guest_unix_call(&mut caller)?
{
return Ok(());
}
}
}
// gexrt's NATIVE_LEFT under Wine's own Unix side (W3), which
// has no NT layer to take it: as `nt::syscall_import` does, the
// innermost native call-out the guest has left is abandoned.
if pc as u64 & nt::NATIVE_SYSCALL != 0 {
if let Some(memory) = caller.data().memory.clone() {
let ctx_addr = caller.data().thread.ctx_addr;
let nr = read_u64(memory.bytes(), (ctx_addr + ctx::x(0) as u64) as usize) as u32;
if nr == gex_kernel::nt::gex_nr::NATIVE_LEFT {
let target = read_u64(memory.bytes(), (ctx_addr + ctx::BALLAST as u64) as usize);
let sp = read_u64(memory.bytes(), (ctx_addr + ctx::x(4) as u64) as usize) & 0xffff_ffff;
if innermost_native_unwound(&caller, sp) {
let _ = GuestMem::new(memory.bytes()).write_u64(ctx_addr + ctx::x(0) as u64, target);
caller.data_mut().callout_signal = Some(CalloutSignal::Abort);
return Err(StopExecution.into());
}
return Ok(());
}
}
}
// An ejected project's runtime allows only the recorded calls.
if let Some(stop) = aot::check_syscall(&mut caller, pc as u64) {
return stop;
@@ -3319,6 +3381,25 @@ fn register_imports(linker: &mut Linker<HostState>) -> Result<()> {
if caller.data().nt.is_some() {
return nt::fault(&mut caller, nt::Fault::NotExecutable, pc).map(|_| 0);
}
// The process's own fault hook (Wine's ntdll.so, W3):
// an access violation, executing (DEP).
if std::env::var_os("GEX_W3_TRACE").is_some() {
if let Some(memory) = caller.data().memory.clone() {
let gm = GuestMem::new(memory.bytes());
let base = caller.data().thread.ctx_addr;
let src = gm.read_u64(base + ctx::LAST_SOURCE as u64).unwrap_or(0);
let esp = gm.read_u64(base + ctx::x(4) as u64).unwrap_or(0) & 0xffff_ffff;
let st: Vec<String> = (0..6).map(|i| format!("{:#x}", gm.read_u32(esp + 4 * i).unwrap_or(0))).collect();
eprintln!("gex-w3: execute at {pc:#x} from block {src:#x}, esp {esp:#x} [{}]", st.join(" "));
let bt = wasmtime::WasmBacktrace::force_capture(&caller);
for fr in bt.frames().iter().take(24) {
eprintln!("gex-w3: {}", fr.func_name().unwrap_or("?"));
}
}
}
if deliver_guest_fault_kind(&mut caller, fault_code::ACCESS_VIOLATION, pc, pc, 8)? {
return Err(StopExecution.into());
}
// Code an ejected project was built without: say so,
// rather than let it read as the guest's own crash.
if let Some(detail) = aot::unseen_code(&mut caller, pc) {
@@ -3400,6 +3481,9 @@ fn register_imports(linker: &mut Linker<HostState>) -> Result<()> {
}
let pc = pc as u64;
let raw = raw as u32;
if deliver_guest_fault(&mut caller, fault_code::ILLEGAL_INSTRUCTION, pc, pc)? {
return Err(StopExecution.into());
}
let memory = caller
.data()
.memory
@@ -3522,6 +3606,10 @@ fn register_imports(linker: &mut Linker<HostState>) -> Result<()> {
// FPE_INTDIV at the dividing instruction.
if imm == gex_abi::imports::BRK_DIVIDE_BY_ZERO || imm == gex_abi::imports::BRK_DIVIDE_OVERFLOW {
let pc = pc as u64;
let code = if imm == gex_abi::imports::BRK_DIVIDE_BY_ZERO { fault_code::INT_DIVIDE_BY_ZERO } else { fault_code::INT_OVERFLOW };
if deliver_guest_fault(&mut caller, code, pc, 0)? {
return Err(StopExecution.into());
}
let memory = caller
.data()
.memory
@@ -3553,6 +3641,9 @@ fn register_imports(linker: &mut Linker<HostState>) -> Result<()> {
// mode): SIGSEGV at the instruction.
if imm == gex_abi::imports::BRK_GENERAL_PROTECTION {
let pc = pc as u64;
if deliver_guest_fault(&mut caller, fault_code::ACCESS_VIOLATION, pc, 0xffff_ffff)? {
return Err(StopExecution.into());
}
let memory = caller
.data()
.memory
@@ -3588,6 +3679,10 @@ fn register_imports(linker: &mut Linker<HostState>) -> Result<()> {
});
return Err(StopExecution.into());
}
// int3: the hook sees the address after it, as hardware leaves it.
if deliver_guest_fault(&mut caller, fault_code::BREAKPOINT, pc as u64 + 1, pc as u64)? {
return Err(StopExecution.into());
}
caller.data_mut().exit = Some(Exit::Signal {
signal: signal::SIGTRAP,
pc: pc as u64,
@@ -3940,6 +4035,10 @@ fn register_imports(linker: &mut Linker<HostState>) -> Result<()> {
match state.with_kernel(|k| k.deliver_pending(&mut gm)) {
gex_kernel::signal::Delivery::None => Ok(()),
gex_kernel::signal::Delivery::Resume(target) => {
if run_signal_frames(&mut caller, pc as u64)? {
return Err(StopExecution.into());
}
let state = caller.data_mut();
state.resume_cause = Some("signal at safepoint".into());
state.resume_at = Some(target);
Err(StopExecution.into())
@@ -4582,7 +4681,10 @@ fn watch_stub(cx: &mut impl wasmtime::AsContextMut<Data = HostState>) -> wasmtim
});
match old {
Some(f) => CalloutSignal::Bypass(f),
None => bail!("a watched dispatch slot with nothing to run for {target:#x}"),
None => bail!(
"a watched dispatch slot with nothing to run for {target:#x} (slot {slot:?}, sp {sp:#x}; call-outs {:?})",
state.callouts.iter().map(|co| (format!("{:#x}", co.ret), format!("{:#x}", co.sp_want), co.slot.as_ref().map(|s| s.0), co.live)).collect::<Vec<_>>()
),
}
}
};
@@ -4630,7 +4732,8 @@ fn refresh_callouts(cx: &mut impl wasmtime::AsContextMut<Data = HostState>) -> R
let n = cx.as_context().data().callouts.len();
for i in 0..n {
let ret = cx.as_context().data().callouts[i].ret;
let want = dispatch_slot_of(&memory, ret).filter(|&s| s < size);
// (only a page with an entry of its own: the miss slots are shared)
let want = dispatch_slot_of(&memory, ret).filter(|&s| s < size && s >= gex_abi::MISS_SLOTS);
let have = cx.as_context().data().callouts[i].slot.as_ref().map(|(s, _)| *s);
if have.is_some() && have != want {
// The return moved: put the old slot back, unless another
@@ -4692,6 +4795,206 @@ pub(crate) fn any_enter(caller: &mut Caller<'_, HostState>) -> Option<wasmtime::
.or_else(|| caller.data().root_enter.clone())
}
/// Guest fault hooks, by process (its kernel's address): a native wasm32
/// program that runs x86 code (Wine's ntdll.so, docs/wine-native.md §4.0
/// W3.1) registers `void hook(int ctx, unsigned code, unsigned addr)`
/// (`gex.fault_hook`), and a fault in its x86 code -- a memory access past
/// the guest's memory, an illegal instruction, int3, a divide error -- is
/// delivered to it instead of ending the process: the context holds the
/// guest state with PC at the fault, the hook leaves the state to go on
/// from (ntdll.so: KiUserExceptionDispatcher's), and the call-out the guest
/// ran under resumes there, the native frames under it intact.
static FAULT_HOOKS: Mutex<Vec<(usize, u32)>> = Mutex::new(Vec::new());
fn fault_hook_of(state: &HostState) -> Option<u32> {
let key = Arc::as_ptr(&state.kernel) as usize;
FAULT_HOOKS.lock().unwrap_or_else(|e| e.into_inner()).iter().find(|(k, _)| *k == key).map(|&(_, h)| h)
}
pub(crate) fn set_fault_hook(state: &HostState, idx: u32) {
let key = Arc::as_ptr(&state.kernel) as usize;
let mut v = FAULT_HOOKS.lock().unwrap_or_else(|e| e.into_inner());
v.retain(|(k, _)| *k != key);
v.push((key, idx));
}
/// A native wasm32 program's signal hook (W3.1), by process: the function
/// that runs the signal frames gex-kernel leaves on the guest's stack
/// (libgex's `__gex_signal_frames(ctx, pc)`, which calls the program's wasm
/// handlers), for a signal that arrives while the thread runs guest code.
static SIGNAL_HOOKS: Mutex<Vec<(usize, u32)>> = Mutex::new(Vec::new());
fn signal_hook_of(state: &HostState) -> Option<u32> {
let key = Arc::as_ptr(&state.kernel) as usize;
SIGNAL_HOOKS.lock().unwrap_or_else(|e| e.into_inner()).iter().find(|(k, _)| *k == key).map(|&(_, h)| h)
}
pub(crate) fn set_signal_hook(state: &HostState, idx: u32) {
let key = Arc::as_ptr(&state.kernel) as usize;
let mut v = SIGNAL_HOOKS.lock().unwrap_or_else(|e| e.into_inner());
v.retain(|(k, _)| *k != key);
v.push((key, idx));
}
/// Run the signal frame gex-kernel just built at a safepoint through the
/// process's signal hook (its handlers are wasm functions, not guest code):
/// the hook's C frames below the frame, the guest resumed where sigreturn
/// put it. False when the process has no hook.
fn run_signal_frames(caller: &mut Caller<'_, HostState>, interrupted: u64) -> Result<bool> {
let Some(hook) = signal_hook_of(caller.data()) else { return Ok(false) };
let memory = caller.data().memory.clone().ok_or_else(|| anyhow!("guest memory unavailable"))?;
let ctx_addr = caller.data().thread.ctx_addr;
let f = crate::ftab::table_func(caller, hook)?;
let typed = f.typed::<(i32, i32), ()>(&*caller).context("gex.signal_hook: the hook is not void(int, unsigned)")?;
let sp = caller.data().jit.as_ref().and_then(|j| j.ftab.as_ref()).and_then(|s| s.provider_sp.clone());
let saved = match &sp {
Some(g) if !gex_abi::mem64() => {
let cur = g.get(&mut *caller).i32().unwrap_or(0) as u32 as u64;
let esp = read_u64(memory.bytes(), (ctx_addr + ctx::X as u64 + 8 * 4) as usize) & 0xffff_ffff;
g.set(&mut *caller, Val::I32(((cur.min(esp).saturating_sub(256)) & !15) as u32 as i32))?;
Some(cur)
}
_ => None,
};
let r = typed.call(&mut *caller, (ctx_addr as i32, interrupted as u32 as i32));
if let (Some(g), Some(cur)) = (&sp, saved) {
g.set(&mut *caller, Val::I32(cur as u32 as i32))?;
}
r?;
let next = read_u64(memory.bytes(), (ctx_addr + ctx::PC as u64) as usize);
caller.data_mut().resume_cause = Some("signal (wasm handler)".into());
caller.data_mut().resume_at = Some(next);
Ok(true)
}
/// Wine's Unix-side system-call dispatcher, by process (W3.1): op 0 sets
/// it, op 1 returns it (0 for none).
static UNIX_DISPATCHERS: Mutex<Vec<(usize, u32)>> = Mutex::new(Vec::new());
pub(crate) fn unix_dispatcher(state: &HostState, op: i32, idx: u32) -> u32 {
let key = Arc::as_ptr(&state.kernel) as usize;
// ops 4 and 5: the Unix-call dispatcher itself, for wasm callers that
// reach the trampoline's address (gexrt calls it directly)
if op >= 4 {
let mut v = NATIVE_UNIX_CALLS.lock().unwrap_or_else(|e| e.into_inner());
if op == 4 {
v.retain(|(k, _)| *k != key);
v.push((key, idx));
return 0;
}
return v.iter().find(|(k, _)| *k == key).map_or(0, |&(_, d)| d);
}
// ops 2 and 3: the same for the guest's Unix calls (the trampoline
// page's `UNIX_CALL_32`, i386 code calling `__wine_unix_call_dispatcher`)
if op >= 2 {
let mut v = GUEST_UNIX_CALLS.lock().unwrap_or_else(|e| e.into_inner());
if op == 2 {
v.retain(|(k, _)| *k != key);
v.push((key, idx));
return 0;
}
return v.iter().find(|(k, _)| *k == key).map_or(0, |&(_, d)| d);
}
let mut v = UNIX_DISPATCHERS.lock().unwrap_or_else(|e| e.into_inner());
if op == 0 {
v.retain(|(k, _)| *k != key);
v.push((key, idx));
return 0;
}
v.iter().find(|(k, _)| *k == key).map_or(0, |&(_, d)| d)
}
/// Wine's `__wine_unix_call_dispatcher` (W3), by process.
static NATIVE_UNIX_CALLS: Mutex<Vec<(usize, u32)>> = Mutex::new(Vec::new());
/// Wine's handler for a Unix call from guest code (W3), by process.
static GUEST_UNIX_CALLS: Mutex<Vec<(usize, u32)>> = Mutex::new(Vec::new());
/// A guest's Unix call (`int 0x80` with EAX = UNIX_CALL, from the
/// trampoline page) in a process whose ntdll.so takes them: its handler
/// runs below the guest's stack, and sets EAX. False when there is none.
fn guest_unix_call(caller: &mut Caller<'_, HostState>) -> Result<bool> {
let hook = unix_dispatcher(caller.data(), 3, 0);
if hook == 0 {
return Ok(false);
}
let memory = caller.data().memory.clone().ok_or_else(|| anyhow!("guest memory unavailable"))?;
let ctx_addr = caller.data().thread.ctx_addr;
let f = crate::ftab::table_func(caller, hook)?;
let typed = f.typed::<i32, ()>(&*caller).context("the guest Unix-call hook is not void(int)")?;
let sp = caller.data().jit.as_ref().and_then(|j| j.ftab.as_ref()).and_then(|s| s.provider_sp.clone());
let saved = match &sp {
Some(g) if !gex_abi::mem64() => {
let cur = g.get(&mut *caller).i32().unwrap_or(0) as u32 as u64;
let esp = read_u64(memory.bytes(), (ctx_addr + ctx::X as u64 + 8 * 4) as usize) & 0xffff_ffff;
g.set(&mut *caller, Val::I32(((cur.min(esp).saturating_sub(256)) & !15) as u32 as i32))?;
Some(cur)
}
_ => None,
};
let r = typed.call(&mut *caller, ctx_addr as i32);
if let (Some(g), Some(cur)) = (&sp, saved) {
g.set(&mut *caller, Val::I32(cur as u32 as i32))?;
}
r?;
Ok(true)
}
/// NTSTATUS codes the hook is given.
pub(crate) mod fault_code {
pub const ACCESS_VIOLATION: u32 = 0xc000_0005;
pub const ILLEGAL_INSTRUCTION: u32 = 0xc000_001d;
pub const BREAKPOINT: u32 = 0x8000_0003;
pub const INT_DIVIDE_BY_ZERO: u32 = 0xc000_0094;
pub const INT_OVERFLOW: u32 = 0xc000_0095;
}
/// Deliver a guest fault to the process's hook (see [`FAULT_HOOKS`]).
/// Returns false when there is none. On true, `resume_at` holds where the
/// guest goes on.
pub(crate) fn deliver_guest_fault(caller: &mut Caller<'_, HostState>, code: u32, pc: u64, addr: u64) -> Result<bool> {
deliver_guest_fault_kind(caller, code, pc, addr, 0)
}
/// [`deliver_guest_fault`] with an access violation's kind (Windows'
/// ExceptionInformation[0]: 0 read, 1 write, 8 execute).
pub(crate) fn deliver_guest_fault_kind(caller: &mut Caller<'_, HostState>, code: u32, pc: u64, addr: u64, kind: u32) -> Result<bool> {
let Some(hook) = fault_hook_of(caller.data()) else { return Ok(false) };
let memory = caller.data().memory.clone().ok_or_else(|| anyhow!("guest memory unavailable"))?;
let ctx_addr = caller.data().thread.ctx_addr;
{
let mut gm = GuestMem::new(memory.bytes());
let _ = gm.write_u64(ctx_addr + ctx::PC as u64, pc);
}
let f = crate::ftab::table_func(caller, hook)?;
let typed = f.typed::<(i32, i32, i32, i32), ()>(&*caller).context("gex.fault_hook: the hook is not void(int, unsigned, unsigned, unsigned)")?;
// The hook's C frames go below the guest's stack pointer, as a signal
// frame does: the shared shadow stack pointer may be wherever a native
// call from the guest last left it, above the live guest stack. The
// red zone leaves the hook room to build the guest's exception frame
// under that stack pointer (i386's is under a KiB).
let sp = caller.data().jit.as_ref().and_then(|j| j.ftab.as_ref()).and_then(|s| s.provider_sp.clone());
let saved = match &sp {
Some(g) if !gex_abi::mem64() => {
let cur = g.get(&mut *caller).i32().unwrap_or(0) as u32 as u64;
let esp = read_u64(memory.bytes(), (ctx_addr + ctx::X as u64 + 8 * 4) as usize) & 0xffff_ffff;
let new = (cur.min(esp).saturating_sub(4096)) & !15;
g.set(&mut *caller, Val::I32(new as u32 as i32))?;
Some(cur)
}
_ => None,
};
let r = typed.call(&mut *caller, (ctx_addr as i32, code as i32, addr as u32 as i32, kind as i32));
if let (Some(g), Some(cur)) = (&sp, saved) {
g.set(&mut *caller, Val::I32(cur as u32 as i32))?;
}
r?;
let next = read_u64(memory.bytes(), (ctx_addr + ctx::PC as u64) as usize);
caller.data_mut().resume_cause = Some(format!("guest fault {code:#x}"));
caller.data_mut().resume_at = Some(next);
Ok(true)
}
/// `gex.call_out` (docs/function-tier.md §7.3): run the guest from `target`
/// until it returns to `ret` with the stack pointer at `sp_want` (or what a
/// callee popped above it), then let the C caller carry on (1); 0 when that
@@ -4705,6 +5008,21 @@ fn call_out(caller: &mut Caller<'_, HostState>, ctx_i: i32, target: u64, ret: u6
let memory = caller.data().memory.clone().ok_or_else(|| anyhow!("guest memory unavailable"))?;
let ctx_addr = ctx_i as u32 as u64;
let unwound = if ret == gex_kernel::nt::boot::tramp::NATIVE_RETURN_PAD { NATIVE_UNWOUND } else { CALLOUT_UNWOUND };
// A return address on a page with no directory entry of its own would
// be watched in the miss slots, which every untranslated page shares:
// translate it first (W3: the trampoline page's NATIVE_RETURN_PAD, which
// no unit covers without the NT layer). A call-out that never returns
// (ret 0: Wine's thread loop) is not watched at all (`refresh_callouts`).
if ret != 0 {
let page = ret / gex_abi::DISPATCH_PAGE;
let entry = |m: &gex_mem::GuestMemory| GuestMem::new(m.bytes()).read_u32(gex_abi::DISPATCH_DIR + page * 4).unwrap_or(1);
if page < gex_abi::DISPATCH_MAX_PAGES && entry(&memory) == 0 {
translate_and_install(caller, ret)?;
if entry(&memory) == 0 && std::env::var_os("GEX_W3_TRACE").is_some() {
eprintln!("gex-w3: call-out return {ret:#x}: its page has still no dispatch entry");
}
}
}
caller.data_mut().callouts.push(Callout { ret, sp_want, unwound, slot: None, live: false });
let me = caller.data().callouts.len() - 1;
refresh_callouts(&mut *caller)?;
@@ -4756,6 +5074,18 @@ fn call_out(caller: &mut Caller<'_, HostState>, ctx_i: i32, target: u64, ret: u6
pc = GuestMem::new(memory.bytes()).read_u64(ctx_addr + ctx::PC as u64).unwrap_or(pc);
continue;
}
// A wild guest access under a process with a fault hook (W3.1):
// delivered, and the guest goes on where the hook says. The
// translated code cannot say which instruction faulted: PC is where
// the context last had it (the block's start, at best).
if matches!(e.downcast_ref::<wasmtime::Trap>(), Some(wasmtime::Trap::MemoryOutOfBounds)) {
let at = GuestMem::new(memory.bytes()).read_u64(ctx_addr + ctx::PC as u64).unwrap_or(pc);
if deliver_guest_fault(caller, fault_code::ACCESS_VIOLATION, at, 0)? {
pc = caller.data_mut().resume_at.take().unwrap_or(at);
refresh_callouts(&mut *caller)?;
continue;
}
}
break Err(anyhow::Error::from(e));
};
// Unwatch: put the slot back unless an install has written over it.
+64
View File
@@ -429,6 +429,70 @@ pub(crate) fn register(linker: &mut Linker<HostState>) -> Result<()> {
};
Ok(())
})()) })?;
// Wine's system-call dispatcher on its Unix side (W3.1): ntdll.so
// registers it (op 0), and the PE side's gexrt asks for it (op 1; 0
// while there is none, and the NT layer takes the call).
linker.func_wrap(IMPORT_MODULE, "unix_dispatcher", |caller: Caller<'_, HostState>, op: i32, idx: i32| -> i32 {
crate::unix_dispatcher(caller.data(), op, idx as u32) as i32
})?;
// Wine's ntdll.so mapped a PE image (W3.1): a native DLL's skeleton gets
// Wine's PE module bound at its exports, as the NT layer's image map
// does (`wasmdll`). The image is named by the descriptor it was mapped
// from.
linker.func_wrap(IMPORT_MODULE, "image_mapped", |mut caller: Caller<'_, HostState>, base: i32, fd: i32| -> wasmtime::Result<()> { crate::host((|| -> anyhow::Result<()> {
let base = base as u32 as u64;
let Some(path) = caller.data_mut().with_kernel(|k| k.fds.get(fd).ok().map(|f| f.path.clone())) else { return Ok(()) };
let Some(bytes) = caller.data_mut().with_kernel(|k| k.read_file(&path)) else { return Ok(()) };
let Ok(pe) = gex_ld::pe::parse(&bytes) else { return Ok(()) };
let (shared, engine) = {
let Some(jit) = caller.data().jit.as_ref() else { return Ok(()) };
(Arc::clone(&jit.shared), jit.engine.clone())
};
let memory = caller.data().memory.clone().ok_or_else(|| anyhow!("guest memory unavailable"))?;
if let Some(n) = crate::wasmdll::install(&pe, &path, base, caller.data_mut(), &shared, &engine, &memory)? {
if std::env::var_os("GEX_W3_TRACE").is_some() {
eprintln!("gex-w3: native module for {path} at {base:#x}: {n} entries");
}
crate::ftab::catch_up(&mut caller)?;
crate::wasmdll::apply_pending(&shared, &memory);
}
Ok(())
})()) })?;
// Code in `[addr, addr + len)` changed (W3: Wine's
// NtFlushInstructionCache): its translations are retired.
linker.func_wrap(IMPORT_MODULE, "code_changed", |mut caller: Caller<'_, HostState>, addr: i32, len: i32| {
let (addr, len) = (addr as u32 as u64, len as u32 as u64);
caller.data_mut().with_kernel(|k| {
let (start, end) = (addr & !0xfff, (addr + len + 0xfff) & !0xfff);
if end > start {
k.retired_code.push((start, end));
}
});
gex_kernel::note_code_change();
})?;
// The function-table index of a named function of Wine's PE module
// (W3.3: win32u.so starts explorer's desktop thread by
// `explorer.exe!gexp_desktop_thread`); 0 when there is none.
linker.func_wrap(IMPORT_MODULE, "wine_pe_slot", |caller: Caller<'_, HostState>, ptr: i32, len: i32| -> i32 {
let Some(m) = caller.data().memory.as_ref() else { return 0 };
let Some(b) = m.bytes().get(ptr as u32 as usize..(ptr as u32 as usize).saturating_add(len as u32 as usize)) else { return 0 };
let name = String::from_utf8_lossy(b).into_owned();
let Some(jit) = caller.data().jit.as_ref() else { return 0 };
let sh = jit.shared.lock().unwrap_or_else(|e| e.into_inner());
let Some(wp) = sh.wine_pe.as_ref() else { return 0 };
match wp.slots.get(&name) {
Some(&k) => (sh.ftab.map.install(wp.install).base + k) as i32,
None => 0,
}
})?;
// A native wasm32 program's signal hook (W3.1, `crate::SIGNAL_HOOKS`).
linker.func_wrap(IMPORT_MODULE, "signal_hook", |caller: Caller<'_, HostState>, idx: i32| {
crate::set_signal_hook(caller.data(), idx as u32);
})?;
// A native wasm32 program's guest fault hook (W3.1, `crate::FAULT_HOOKS`).
linker.func_wrap(IMPORT_MODULE, "fault_hook", |caller: Caller<'_, HostState>, idx: i32| {
crate::set_fault_hook(caller.data(), idx as u32);
})?;
// Diagnostics: text a provider writes to its stderr.
linker.func_wrap(IMPORT_MODULE, "provider_log", |caller: Caller<'_, HostState>, ptr: i32, len: i32| -> wasmtime::Result<()> { crate::host((|| -> anyhow::Result<()> {
if let Some(m) = caller.data().memory.as_ref() {
+29 -10
View File
@@ -86,9 +86,9 @@ pub(crate) fn install(
/// install, where its data is, its slots by name and its data exports'
/// offsets.
pub(crate) struct WinePe {
install: u32,
pub(crate) install: u32,
data: u64,
slots: std::collections::HashMap<String, u32>,
pub(crate) slots: std::collections::HashMap<String, u32>,
/// `gexp_<export>` slots by export name, whichever DLL owns them: a
/// DLL's `-import` re-exports are served by the owner's adapter.
any_dll: std::collections::HashMap<String, u32>,
@@ -135,15 +135,34 @@ fn bind_entry_thunks(wp: &WinePe, shared: &Arc<Mutex<JitShared>>, memory: &gex_m
return Ok(());
}
let base = shared.lock().unwrap_or_else(|e| e.into_inner()).ftab.map.install(wp.install).base;
let mut pairs = Vec::with_capacity(((stop - start) / 8) as usize);
let mut a = start;
while a + 8 <= stop {
let (f, t) = (memory_u32(memory, a)?, memory_u32(memory, a + 4)?);
a += 8;
if f < base || t < base || f as u64 >= 0x3f0000 {
continue;
let read = |start: u64, stop: u64| -> Result<Vec<(u64, u32)>> {
let mut out = Vec::with_capacity(((stop.saturating_sub(start)) / 8) as usize);
let mut a = start;
while a + 8 <= stop {
let (f, t) = (memory_u32(memory, a)?, memory_u32(memory, a + 4)?);
a += 8;
if f < base || t < base || f as u64 >= 0x3f0000 {
continue;
}
out.push((f as u64, t - base));
}
Ok(out)
};
let mut pairs = read(start, stop)?;
// Thunks made where a function is handed out but not defined: only for
// functions no defining object gave one (it knows the convention).
if let Some(&off) = wp.data_exports.get("__gexw_entries_decl") {
let at = wp.data + off as u64;
let (s2, e2) = (memory_u32(memory, at)? as u64, memory_u32(memory, at + 4)? as u64);
if s2 != 0 && e2 > s2 {
let strong: std::collections::HashSet<u64> = pairs.iter().map(|&(f, _)| f).collect();
let mut seen = std::collections::HashSet::new();
for (f, t) in read(s2, e2)? {
if !strong.contains(&f) && seen.insert(f) {
pairs.push((f, t));
}
}
}
pairs.push((f as u64, t - base));
}
if std::env::var_os("GEX_WASM_TRACE").is_some() {
eprintln!("gex: wine PE: {} entry thunks bound", pairs.len());
+22
View File
@@ -41,6 +41,28 @@ pub(crate) fn is_wasm32(program: &Program) -> bool {
})
}
/// Header flag (offset 36): the program runs i386 code (Wine, W3), so the
/// process translates i386 and leaves Windows' fixed addresses free.
pub const FLAG_GUEST_I386: u32 = 1;
/// The header's flags, 0 for a program without them (or not wasm32).
pub(crate) fn flags(program: &Program) -> u32 {
let at = program.exe.entry.wrapping_sub(0x40);
program
.exe
.segments
.iter()
.find(|s| at >= s.vaddr && at + 40 <= s.vaddr + s.bytes.len() as u64)
.map(|s| {
let o = (at - s.vaddr) as usize;
if &s.bytes[o..o + 8] != MAGIC {
return 0;
}
u32::from_le_bytes(s.bytes[o + 36..o + 40].try_into().unwrap())
})
.unwrap_or(0)
}
/// If the program just placed is a native wasm32 one, install its module
/// in the function table at its entries. Returns whether it was.
pub(crate) fn install(store: &mut Store<HostState>, memory: &gex_mem::GuestMemory, program: &Program) -> Result<bool> {
Binary file not shown.
Binary file not shown.
+7 -1
View File
@@ -62,8 +62,14 @@ int main(void) {
CHECK("the screen metrics follow", GetSystemMetrics(SM_CXSCREEN) == 800 && GetSystemMetrics(SM_CYSCREEN) == 600);
DEVMODEA cur = {.dmSize = sizeof cur};
CHECK("ENUM_CURRENT_SETTINGS is the new mode", EnumDisplaySettingsA(NULL, ENUM_CURRENT_SETTINGS, &cur) && cur.dmPelsWidth == 800);
/* (Wine tells the desktop's thread with a notify message: a desktop
* in another thread, as explorer's, takes the size a moment later) */
RECT dr;
GetWindowRect(GetDesktopWindow(), &dr);
for (int i = 0; i < 100; i++) {
GetWindowRect(GetDesktopWindow(), &dr);
if (dr.right == 800 && dr.bottom == 600) break;
Sleep(10);
}
CHECK("the desktop window takes its size", dr.right == 800 && dr.bottom == 600);
CHECK("restore", ChangeDisplaySettingsA(NULL, 0) == DISP_CHANGE_SUCCESSFUL);
CHECK("the metrics are back", GetSystemMetrics(SM_CXSCREEN) == w0 && GetSystemMetrics(SM_CYSCREEN) == h0);
+25
View File
@@ -0,0 +1,25 @@
/* The first thread ends with exit (pthread_exit, not exit_group) while
* another runs: the process goes on until that one ends it, with its code
* (Linux; Wine's ExitProcess from a second thread does exactly this). */
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
static void *worker(void *arg)
{
(void)arg;
usleep(50000);
printf("worker still running\n");
fflush(stdout);
exit(33);
}
int main(void)
{
pthread_t t;
pthread_create(&t, NULL, worker, NULL);
printf("main thread exiting\n");
fflush(stdout);
pthread_exit(NULL);
}
+63
View File
@@ -0,0 +1,63 @@
/* UDP over the loopback between guests: a server bound to 127.0.0.1 (an
* ephemeral port), a client that sends unbound (and so gets a port), the
* reply to the address recvfrom reported, a connected socket's send, a
* datagram from a forked child, and EADDRINUSE on the bound port. Prints
* nothing that depends on which ports. */
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void) {
int srv = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in a = {.sin_family = AF_INET, .sin_port = 0, .sin_addr.s_addr = htonl(INADDR_LOOPBACK)};
printf("bind %d\n", bind(srv, (struct sockaddr *)&a, sizeof a));
struct sockaddr_in where;
socklen_t wl = sizeof where;
getsockname(srv, (struct sockaddr *)&where, &wl);
printf("bound port nonzero %d\n", where.sin_port != 0);
int dup = socket(AF_INET, SOCK_DGRAM, 0);
int r = bind(dup, (struct sockaddr *)&where, sizeof where);
printf("second bind: %d %s\n", r, r && errno == EADDRINUSE ? "EADDRINUSE" : "");
close(dup);
int cli = socket(AF_INET, SOCK_DGRAM, 0);
printf("sendto %zd\n", sendto(cli, "ping", 4, 0, (struct sockaddr *)&where, sizeof where));
struct sockaddr_in me;
socklen_t ml = sizeof me;
getsockname(cli, (struct sockaddr *)&me, &ml);
char buf[64] = {0};
struct sockaddr_in from;
socklen_t fl = sizeof from;
ssize_t n = recvfrom(srv, buf, sizeof buf - 1, 0, (struct sockaddr *)&from, &fl);
printf("server got %zd [%s] from 127.0.0.1 %d, the client's port %d\n", n, buf, from.sin_addr.s_addr == htonl(INADDR_LOOPBACK), from.sin_port == me.sin_port);
sendto(srv, "pong", 4, 0, (struct sockaddr *)&from, fl);
memset(buf, 0, sizeof buf);
n = recv(cli, buf, sizeof buf - 1, 0);
printf("client got %zd [%s]\n", n, buf);
int con = socket(AF_INET, SOCK_DGRAM, 0);
printf("connect %d\n", connect(con, (struct sockaddr *)&where, sizeof where));
printf("send %zd\n", send(con, "connected", 9, 0));
memset(buf, 0, sizeof buf);
n = recv(srv, buf, sizeof buf - 1, 0);
printf("server got %zd [%s]\n", n, buf);
fflush(stdout);
pid_t c = fork();
if (c == 0) {
int k = socket(AF_INET, SOCK_DGRAM, 0);
sendto(k, "from the child", 14, 0, (struct sockaddr *)&where, sizeof where);
_exit(0);
}
memset(buf, 0, sizeof buf);
n = recv(srv, buf, sizeof buf - 1, 0);
printf("server got %zd [%s]\n", n, buf);
int st;
waitpid(c, &st, 0);
return 0;
}
+36 -17
View File
@@ -315,14 +315,40 @@ struct GexIcall : PassInfoMixin<GexIcall> {
return T;
}
// The pairs as a used, internal array in `section`.
static void emitPairs(Module &M, const std::vector<std::pair<Function *, Function *>> &pairs,
const char *name, const char *section) {
if (pairs.empty()) return;
LLVMContext &C = M.getContext();
Type *Ptr = PointerType::get(C, 0);
StructType *PairT = StructType::get(Ptr, Ptr);
std::vector<Constant *> elems;
for (auto &[F, T] : pairs) elems.push_back(ConstantStruct::get(PairT, {F, T}));
ArrayType *AT = ArrayType::get(PairT, elems.size());
auto *G = new GlobalVariable(M, AT, true, GlobalValue::InternalLinkage, ConstantArray::get(AT, elems), name);
G->setSection(section);
G->setAlignment(Align(4));
appendToUsed(M, {G});
}
unsigned entryThunks(Module &M) {
std::map<Function *, Conv> cv = conventions(M);
std::vector<std::pair<Function *, Function *>> pairs;
std::vector<Function *> fs;
std::vector<std::pair<Function *, Function *>> pairs, decl_pairs;
std::vector<Function *> fs, decls;
for (Function &F : M) {
if (F.isDeclaration() || F.hasFnAttribute("gexw-no-icall") || F.isIntrinsic()) continue;
if (F.hasFnAttribute("gexw-no-icall") || F.isIntrinsic()) continue;
StringRef n = F.getName();
if (n.starts_with("__gexw") || n.starts_with("__gexe.") || n.contains("gexp_")) continue;
if (n.starts_with("__gexw") || n.starts_with("__gexe.") || n.contains("gexp_") || n.starts_with("llvm.")) continue;
if (F.isDeclaration()) {
// A function defined elsewhere whose address this object hands out
// (a table of server routines next to their definitions in another
// object, a function imported from another DLL): a thunk of its
// own, which gex binds only when no object defining the function
// gave one (`gexw_entries_decl`): the definition knows its
// convention, this object may not.
if (escapes(F)) decls.push_back(&F);
continue;
}
// (a thiscall method may be referenced only from a vtable written in
// assembly, which the IR does not show: msvcrt's and msvcp's)
bool method = cv.count(&F) && cv[&F] == THISCALL && !F.hasLocalLinkage();
@@ -331,19 +357,12 @@ struct GexIcall : PassInfoMixin<GexIcall> {
for (Function *F : fs)
if (Function *T = entryThunk(M, *F, cv.count(F) ? cv[F] : CDECL))
pairs.push_back({F, T});
if (pairs.empty()) return 0;
LLVMContext &C = M.getContext();
Type *Ptr = PointerType::get(C, 0);
StructType *PairT = StructType::get(Ptr, Ptr);
std::vector<Constant *> elems;
for (auto &[F, T] : pairs) elems.push_back(ConstantStruct::get(PairT, {F, T}));
ArrayType *AT = ArrayType::get(PairT, elems.size());
auto *G = new GlobalVariable(M, AT, true, GlobalValue::InternalLinkage, ConstantArray::get(AT, elems),
"__gexe.pairs");
G->setSection("gexw_entries");
G->setAlignment(Align(4));
appendToUsed(M, {G});
return pairs.size();
for (Function *F : decls)
if (Function *T = entryThunk(M, *F, cv.count(F) ? cv[F] : CDECL))
decl_pairs.push_back({F, T});
emitPairs(M, pairs, "__gexe.pairs", "gexw_entries");
emitPairs(M, decl_pairs, "__gexe.declpairs", "gexw_entries_decl");
return pairs.size() + decl_pairs.size();
}
PreservedAnalyses run(Module &M, ModuleAnalysisManager &) {
+110 -2
View File
@@ -24,6 +24,8 @@ import sys
import tempfile
here = os.path.dirname(os.path.realpath(__file__))
EAGER = {} # module -> the DLLs to load before it (pelink's main)
SRC = None # the Wine source tree (pelink's main)
def enc_leb(v):
@@ -50,6 +52,50 @@ def makefile_var(path, var):
return m.group(1).split() if m else []
def spec_path(src, top, d, mod):
"""A module's .spec: X.spec for X.dll, else the full name (ntoskrnl.exe.spec)."""
for name in (mod.rsplit('.', 1)[0] + '.spec', mod + '.spec'):
p = os.path.join(src, top, d, name)
if os.path.exists(p):
return p
return os.path.join(src, top, d, mod + '.spec')
def spec_stdcalls(spec):
"""The names a .spec exports as stdcall."""
out = set()
try:
lines = open(spec).read().splitlines()
except OSError:
return out
for line in lines:
parts = line.split('#', 1)[0].split()
if len(parts) >= 3 and parts[1] == 'stdcall':
rest = [t for t in parts[2:] if not t.startswith('-')]
if rest:
out.add(rest[0].split('(', 1)[0])
return out
def spec_functions(spec):
"""The function names a .spec exports (its stdcall/cdecl/thiscall/
varargs entries; not data, stubs or extern)."""
try:
lines = open(spec).read().splitlines()
except OSError:
return []
out = []
for line in lines:
line = line.split('#', 1)[0].strip()
parts = line.split()
if len(parts) < 3 or parts[1] not in ('stdcall', 'cdecl', 'thiscall', 'varargs', 'fastcall'):
continue
rest = [t for t in parts[2:] if not t.startswith('-')]
if rest:
out.append(rest[0].split('(', 1)[0])
return out
def main():
if sys.argv[1] == '--gexrt':
# explicit inputs (tests)
@@ -69,6 +115,8 @@ def main():
sys.exit(f'unknown argument {args[i]}')
return link(dlls, out, gexrt)
build, src, out = sys.argv[1:4]
global SRC
SRC = src
only = set(sys.argv[4:])
dlls = [] # (dll file name, r.o, exports, imports)
for top in ('dlls', 'programs'):
@@ -87,17 +135,36 @@ def main():
imports = [i.lstrip('-') for i in makefile_var(mk, 'IMPORTS') + makefile_var(mk, 'DELAYIMPORTS')]
imports = [i if '.' in i else i + '.dll' for i in imports]
dlls.append((mod, ro, ex, imports))
# what the loader is to load before the module (makedep's
# direct imports and default ones: the C runtime, kernel32,
# ntdll), which a skeleton's empty import table cannot say
eager = [i.lstrip('-') for i in makefile_var(mk, 'IMPORTS')]
if '-nodefaultlibs' not in makefile_var(mk, 'EXTRADLLFLAGS'):
is_crt = lambda n: n.startswith(('msvcr', 'ucrt')) or n == 'crtdll'
crt = next((i for i in eager if is_crt(i)), None if is_crt(mod.split('.')[0]) else 'ucrtbase')
eager += [c for c in (crt, 'kernel32', 'ntdll') if c and c not in eager]
EAGER[mod] = [i if '.' in i else i + '.dll' for i in eager]
if not dlls:
sys.exit('no DLL objects found (build with winegcc for wasm32)')
# every DLL the build has an export list for, native or not: what an
# import the linked DLLs leave undefined is found in (guest imports)
others = {}
spec_dir = tempfile.mkdtemp(prefix='pelink-specs-')
for top in ('dlls',):
for d in sorted(os.listdir(os.path.join(build, top))):
mod = makefile_var(os.path.join(src, top, d, 'Makefile.in'), 'MODULE')
ex = os.path.join(build, top, d, mod[0] + '.so.exports') if mod else ''
if mod and os.path.exists(ex):
others[mod[0]] = ex
elif mod:
# a DLL whose wasm32 build failed is still an i386 one in
# the prefix: its .spec names its functions
names = spec_functions(spec_path(src, top, d, mod[0]))
if names:
ex = os.path.join(spec_dir, mod[0] + '.exports')
with open(ex, 'w') as f:
f.writelines(f'{n} func\n' for n in names)
others[mod[0]] = ex
link(dlls, out, os.path.join(build, 'libs', 'gexrt', 'libgexrt.a'), others)
@@ -207,6 +274,11 @@ def link(dlls, out, gexrt, others=None):
f.write(f'{name} {pick}\n')
bound = os.path.join(tmp, mod + '.o')
subprocess.check_call([sys.executable, os.path.join(here, 'pesym.py'), 'imports', local, bound, mod, mapfile])
# The ICALL pass's stdcall annotations are for the compiler only;
# wasm-ld 22 crashes relocating them when a function they name is
# not linked (services.exe, plugplay.exe).
subprocess.check_call(['llvm-objcopy', '--remove-section=llvm.func_attr.annotate.gex.stdcall',
'--remove-section=reloc.llvm.func_attr.annotate.gex.stdcall', bound])
objs.append(bound)
if thunks:
@@ -266,27 +338,63 @@ def link(dlls, out, gexrt, others=None):
own = lambda n: next((d for d in ('kernelbase.dll', 'kernel32.dll') if d in exporters.get(n, [])), 'kernel32.dll')
f.write(f'void *gexw_LoadLibraryA(const char *) __asm__("{own("LoadLibraryA")}!LoadLibraryA");\n')
f.write(f'void *gexw_GetProcAddress(void *, const char *) __asm__("{own("GetProcAddress")}!GetProcAddress");\n')
stdcalls = {}
for k, (n, sig, owner) in enumerate(stubs):
if owner not in stdcalls:
stdcalls[owner] = spec_stdcalls(spec_path(SRC, 'dlls', owner, owner)) if SRC else set()
# a stdcall entry thunk (callee pops) for i386 callers
# of the stub's table index (below)
attr = '__attribute__((annotate("gex.stdcall"))) ' if n in stdcalls[owner] else ''
r, ps = sig.split(':')
params = ', '.join(f'{ctype[c]} a{i}' for i, c in enumerate(ps)) or 'void'
types = ', '.join(ctype[c] for c in ps) or 'void'
args = ', '.join(f'a{i}' for i in range(len(ps)))
f.write(f'{ctype[r]} g{k}({params}) __asm__("{n}");\n')
f.write(f'{ctype[r]} g{k}({params}) {{\n'
f.write(f'{attr}{ctype[r]} g{k}({params}) __asm__("{n}");\n')
f.write(f'{attr}{ctype[r]} g{k}({params}) {{\n'
f' static void *p;\n'
f' if (!p) p = gexw_GetProcAddress(gexw_LoadLibraryA("{owner}"), "{n}");\n'
f' {"" if r == "v" else "return "}(({ctype[r]} (*)({types}))p)({args});\n}}\n')
# Their addresses taken, so the ICALL pass gives each an entry
# thunk: a native module may hand one to i386 code as a
# function pointer (services.exe's RPC dispatch table holds
# rpcrt4's NdrServerCall2), which then calls its table index.
f.write('void *const __gexw_guestimport_refs[] __attribute__((used)) = {\n')
for k in range(len(stubs)):
f.write(f' (void *)g{k},\n')
f.write('};\n')
obj = os.path.join(tmp, 'guestimports.o')
subprocess.check_call([os.path.join(here, 'wcc'), '-O2', '-D_WIN32', '-c', '-o', obj, src])
objs.append(obj)
print(f'{out}: {len(stubs)} imports from i386 DLLs ({", ".join(sorted({o for _, _, o in stubs}))})')
# Each module's load-time imports for Wine's PE loader on wasm32
# (ntdll's fixup_imports asks __gexw_module_imports): only DLLs.
if EAGER:
known = {m for m, _, _, _ in dlls} | set(others or {})
src = os.path.join(tmp, 'moduleimports.c')
with open(src, 'w') as f:
f.write('static const char *const gexw_imports[] = {\n')
for mod in sorted(EAGER):
deps = [d for d in EAGER[mod] if d in known and d != mod]
f.write(' "%s", "%s",\n' % (mod, ''.join(d + '\\0' for d in deps)))
f.write(' 0\n};\n')
f.write('const char *__gexw_module_imports(const unsigned short *name)\n{\n'
' for (int i = 0; gexw_imports[i]; i += 2) {\n'
' const char *a = gexw_imports[i]; const unsigned short *b = name;\n'
' while (*a && *b && (*a | 0x20) == ((*b < 128 ? *b : 0) | 0x20)) a++, b++;\n'
' if (!*a && !*b) return gexw_imports[i + 1];\n'
' }\n return 0;\n}\n')
obj = os.path.join(tmp, 'moduleimports.o')
subprocess.check_call([os.path.join(here, 'wcc'), '-O2', '-D_WIN32', '-c', '-o', obj, src])
objs.append(obj)
rsp = os.path.join(tmp, 'exports.rsp')
with open(rsp, 'w') as f:
for d in data:
f.write(f'--export-if-defined={d}\n')
# gexrt's bounds of the entry-thunk pairs (gexw_entries)
f.write('--export-if-defined=__gexw_entries\n')
f.write('--export-if-defined=__gexw_entries_decl\n')
cmd = ['wasm-ld', '--experimental-pic', '-shared', '--shared-memory', '--import-memory',
'--max-memory=4294967296', '--unresolved-symbols=import-dynamic', '-o', out,
f'@{rsp}'] + objs + [gexrt]
+3 -2
View File
@@ -136,7 +136,7 @@ def rewrite(b, fix, aliases=()):
nm, s = name(b, s)
align, s = leb(b, s)
flags, s = leb(b, s)
if nm == 'gexw_entries':
if nm in ('gexw_entries', 'gexw_entries_decl'):
flags |= WASM_SEG_FLAG_RETAIN
segs += enc_name(nm) + enc(align) + enc(flags)
body += bytes([sub]) + enc(len(segs)) + segs; r = subend; continue
@@ -293,7 +293,8 @@ RUNTIME = ('__gexw_', 'gexw_', '__wasm_', '__c_longjmp', '__stack_pointer',
def is_runtime(nm):
return nm is not None and (nm.startswith(RUNTIME) and not nm.startswith('__gexw_adapters_'))
# (each DLL's own: its adapters, and the base gex writes for winecrt0)
return nm is not None and (nm.startswith(RUNTIME) and not nm.startswith(('__gexw_adapters_', '__gexw_image_base')))
def main():
+62
View File
@@ -0,0 +1,62 @@
/* W3.1: faults in x86 code a native wasm32 program runs reach its fault
* hook (gex.fault_hook), which points the guest at a recovery stub; the
* call-out the code ran under (gex.call_out) then returns normally, the
* native frames under it intact. One case per fault kind. */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <sys/mman.h>
#include "wine/gex/gex_abi.h"
#include "wine/gex/provider.h"
unsigned long __gex_get_ctx(void);
__attribute__((import_module("gex"), import_name("fault_hook"))) void gex_fault_hook(int idx);
static uint32_t recover; /* x86: mov eax, 7; ret */
static unsigned last_code, last_pc;
static void hook(int ctx, unsigned code, unsigned addr, unsigned kind)
{
(void)addr;
last_code = code;
last_pc = (unsigned)GEX_C64(GEX_CTX_PC);
GEX_C64(GEX_CTX_PC) = recover;
}
static int run(uint32_t target)
{
int ctx = (int)__gex_get_ctx();
uint32_t sp0 = gp_reg(ctx, R_ESP), sp = (sp0 - 64) & ~15u, ra = target ^ 1u;
GW(sp) = ra;
gp_set(ctx, R_ESP, sp);
gp_set(ctx, R_EAX, 0);
int ok = gex_call_out(ctx, target, ra, sp + 4);
unsigned eax = gp_reg(ctx, R_EAX);
gp_set(ctx, R_ESP, sp0);
return ok ? (int)eax : -1;
}
int main(void)
{
static const unsigned char rec[] = { 0xb8, 7, 0, 0, 0, 0xc3 };
static const unsigned char mem[] = { 0xb8, 0x34, 0x12, 0, 0, 0xa1, 0, 0xf0, 0xff, 0xff, 0xc3 }; /* mov eax,[0xfffff000] */
static const unsigned char brk[] = { 0x90, 0xcc, 0xc3 }; /* int3 */
static const unsigned char ill[] = { 0x90, 0x0f, 0x0b, 0xc3 }; /* ud2 */
static const unsigned char div[] = { 0x31, 0xc9, 0x31, 0xd2, 0xb8, 1, 0, 0, 0, 0xf7, 0xf1, 0xc3 }; /* div ecx (0) */
unsigned char *p = mmap(0, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
memcpy(p, rec, sizeof rec);
memcpy(p + 0x100, mem, sizeof mem);
memcpy(p + 0x200, brk, sizeof brk);
memcpy(p + 0x300, ill, sizeof ill);
memcpy(p + 0x400, div, sizeof div);
mprotect(p, 4096, PROT_READ | PROT_EXEC);
recover = (uint32_t)(uintptr_t)p;
gex_fault_hook((int)(uintptr_t)hook);
static const char *names[] = { "memory", "int3", "ud2", "divide" };
for (int i = 0; i < 4; i++) {
last_code = 0;
int r = run(recover + 0x100 * (i + 1));
printf("%s: returned %d, fault %#x at +%#x\n", names[i], r, last_code, last_pc ? last_pc - recover : 0);
}
return 0;
}
+37
View File
@@ -0,0 +1,37 @@
/* W3.0 spike: a native wasm32 program (the shape of Wine's loader) maps x86
* code and calls it through gex's function table, as ntdll.so will call
* the program's code. x86: mov eax,[esp+4]; add eax,[esp+8]; ret */
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <sys/mman.h>
#include "wine/gex/gex_abi.h"
#include "wine/gex/provider.h"
unsigned long __gex_get_ctx(void);
static uint64_t guest_call(uint32_t target, const uint32_t *words, uint32_t n)
{
int ctx = (int)__gex_get_ctx();
uint32_t sp0 = gp_reg(ctx, R_ESP);
uint32_t sp = (sp0 - 4 * n - 4 - 64) & ~15u;
uint32_t ra = target ^ 1u;
GW(sp) = ra;
for (uint32_t i = 0; i < n; i++) GW(sp + 4 + 4 * i) = words[i];
gp_set(ctx, R_ESP, sp);
GEX_C64(GEX_CTX_LAST_TARGET) = target;
int ok = GEX_FTAB(gex_ftab_idx(target))(ctx, 1, 0xffffffffu, ra);
gp_set(ctx, R_ESP, sp0);
if (!ok) printf("guest left another way, pc %#llx\n", (unsigned long long)GEX_C64(GEX_CTX_PC));
return gp_reg(ctx, R_EAX);
}
int main(void)
{
static const unsigned char code[] = { 0x8b, 0x44, 0x24, 0x04, 0x03, 0x44, 0x24, 0x08, 0xc3 };
unsigned char *p = mmap(0, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
memcpy(p, code, sizeof code);
mprotect(p, 4096, PROT_READ | PROT_EXEC);
uint32_t args[2] = { 40, 2 };
printf("x86 add(40, 2) = %llu\n", (unsigned long long)guest_call((uint32_t)(uintptr_t)p, args, 2));
return 0;
}
+15 -3
View File
@@ -25,9 +25,15 @@ winebuild=${WINEBUILD:-$(grep -m1 -o '[^ ]*/tools/winebuild/winebuild' "$build/M
rm -rf "$out"
mkdir -p "$out"
# symbolic links: the prefix may be on another file system
# symbolic links: the prefix may be on another file system. Only for the
# large, read-only parts (the DLLs and programs, fonts, NLS tables): every
# other file is copied, so a run that writes one (the registry, win.ini,
# INetCookies/index.dat) writes this prefix's copy, never the staged
# prefix every other prefix links to.
cp -rs "$x86/." "$out/"
rm -f "$out/gex-layout"
find "$out" -type l ! -iregex '.*\.\(dll\|exe\|drv\|sys\|ocx\|acm\|cpl\|ax\|tlb\|nls\|ttf\|ttc\|fon\|mui\|so\|wasm\|dll16\|exe16\|drv16\|vxd\)' -print0 |
while IFS= read -r -d '' f; do cp --remove-destination "$(readlink -f "$f")" "$f"; done
sys=$out/drive_c/windows/syswow64
tmp=$(mktemp -d "${TMPDIR:-/tmp}/stage-native.XXXXXX")
trap 'rm -rf "$tmp"' EXIT
@@ -37,11 +43,17 @@ cp "$tmp/winepe.wasm" "$sys/winepe.wasm"
for dll in "$@"; do
name=${dll%.*}
dir=$build/dlls/$name
[ -d "$dir" ] || dir=$build/dlls/$dll # (winewayland.drv's directory keeps the extension)
[ -d "$dir" ] || dir=$build/programs/$name
spec=$src/${dir#$build/}/$name.spec
rm -f "$sys/$dll"
# a program has no .spec: an exe skeleton, its entry point the module's
# (a DLL with no .spec, a driver like winewayland.drv, exports nothing:
# an empty one, so its skeleton is still a DLL with DllMain as entry)
if [ ! -f "$spec" ] && [ "${dir#$build/dlls/}" != "$dir" ]; then spec=$tmp/$name.spec; : > "$spec"; fi
if [ -f "$spec" ]; then kind=(--dll -E "$spec"); else kind=(--exe); fi
# shellcheck disable=SC2046
"$winebuild" -w -b wasm32-unknown-linux-musl --cc-cmd="$here/wcc" --dll --fake-module \
--wasm-shared winepe.wasm -E "$spec" -F "$dll" -o "$sys/$dll" $(ls "$dir"/*.res 2>/dev/null)
"$winebuild" -w -b wasm32-unknown-linux-musl --cc-cmd="$here/wcc" "${kind[@]}" --fake-module \
--wasm-shared winepe.wasm -F "$dll" -o "$sys/$dll" $(ls "$dir"/*.res 2>/dev/null)
done
echo "$out: ${*} native ($(du -h "$sys/winepe.wasm" | cut -f1) module)"
+6 -2
View File
@@ -56,7 +56,11 @@ def up(v, a):
def main():
src, dst = sys.argv[1], sys.argv[2]
args = [a for a in sys.argv[1:] if not a.startswith('--')]
src, dst = args[0], args[1]
# --guest-i386: the program runs i386 code (Wine's loader, W3): gex
# translates i386 in its process and leaves Windows' addresses free.
flags = 1 if '--guest-i386' in sys.argv[1:] else 0
w = open(src, 'rb').read()
mem_size, mem_align = dylink_mem(w)
ehsize, phsize, nph = 52, 32, 5
@@ -69,7 +73,7 @@ def main():
data_va = up(code_va + PAGE, max(PAGE, 1 << mem_align))
data_sz = up(max(mem_size, 16), PAGE)
hdr = b'GEXWASM\0' + struct.pack('<7I', 1, BASE + wasm_off, len(w), data_va, data_sz, entry, entry + 16)
hdr = b'GEXWASM\0' + struct.pack('<8I', 1, BASE + wasm_off, len(w), data_va, data_sz, entry, entry + 16, flags)
text = hdr.ljust(0x40, b'\0') + b'\xf4' * 32
text = text.ljust(PAGE, b'\xf4')
+6 -2
View File
@@ -45,6 +45,7 @@ out = 'a.out'
srcs, ld_in, ld_flags, cflags = [], [], [], []
shared = False
relocatable = False
guest_i386 = False
i = 0
while i < len(args):
a = args[i]
@@ -56,7 +57,8 @@ while i < len(args):
relocatable = True
elif a.startswith('-Wl,'):
# (lld needs no archive groups: it searches archives as needed)
ld_flags += [x for x in a[4:].split(',') if x and x not in ('--start-group', '--end-group', '--sort-common', '--sort-section', 'alignment', '--warn-common')]
ld_flags += [x for x in a[4:].split(',') if x and x not in ('--start-group', '--end-group', '--sort-common', '--sort-section', 'alignment', '--warn-common', '--gex-guest-i386')]
guest_i386 = guest_i386 or '--gex-guest-i386' in a[4:].split(',')
elif a.startswith('-l') or a.startswith('-L'):
if a in ('-l', '-L'):
ld_in += [a + args[i + 1]]; i += 2; continue
@@ -188,4 +190,6 @@ r = subprocess.call(cmd)
if r:
sys.exit(r)
finish(wasm)
sys.exit(subprocess.call([sys.executable, os.path.join(here, 'wasm-elf.py'), wasm, out]))
# -Wl,--gex-guest-i386 (taken out of the wasm-ld flags above): the container
# says the program runs i386 code (Wine's loader).
sys.exit(subprocess.call([sys.executable, os.path.join(here, 'wasm-elf.py'), wasm, out] + (['--guest-i386'] if guest_i386 else [])))