mirror of
https://github.com/HeyPuter/gex.git
synced 2026-11-13 20:12:43 -10:00
Merge commit '5a066f67' into merge/unity
This commit is contained in:
commit
ba7d2dc7c0
31 files changed
+1928
-94
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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 "";
|
||||
}
|
||||
|
||||
@@ -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 } : {}),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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`).
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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());
|
||||
|
||||
@@ -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.
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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]
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)"
|
||||
@@ -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
@@ -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 [])))
|
||||
Reference in new issue
Block a user