# B09 — memoryUnix: JIT pages mapped PROT_READ|PROT_WRITE|PROT_EXEC permanently; W^X defeated

Bug ref      : always.md B.9 ; pharo.md §5.24
Severity     : HIGH (any OOB write into JIT region becomes arbitrary RCE)
File         : src/memoryUnix.c
Lines (HEAD) : 65-90 (`sqMakeMemoryExecutableFromTo` / `…NotExecutableFromTo` bodies commented out),
               93-117 (`allocateJITMemory` mmaps with PROT_READ|WRITE|EXEC)

## Problem

```c
void
sqMakeMemoryExecutableFromTo(unsigned long startAddr, unsigned long endAddr)
{
//	sqInt firstPage = roundDownToPage(startAddr);
//	if (mprotect((void *)firstPage,
//				 endAddr - firstPage + 1,
//				 PROT_READ | PROT_WRITE | PROT_EXEC) < 0){
//		...
//	}
}

void
sqMakeMemoryNotExecutableFromTo(...) {
//	... same, commented out
}

void* allocateJITMemory(usqInt desiredSize, usqInt desiredPosition){
	...
	mmap(..., PROT_READ | PROT_WRITE | PROT_EXEC, ...)
```

JIT pages are mapped writable **and** executable for the entire VM
lifetime. The hooks designed to flip the region between
R+W (writing code) and R+X (running code) are no-ops because the
bodies are commented out. Any out-of-bounds write that lands inside
the JIT region therefore becomes arbitrary code execution: the
attacker simply writes shellcode into the JIT page and waits for the
VM to jump there.

The Windows port (`sqWin32SpurAlloc.c:200-227`, COGVM) does the
right thing by calling `VirtualProtect` to toggle between
`PAGE_READWRITE` and `PAGE_EXECUTE_READWRITE`. The Unix port lost
this in a refactor and the comment in the source admits the
intention.

## Fix (two-part, conservative)

This PR enables the hooks but only switches the underlying mmap to
`PROT_READ|PROT_WRITE` (no X) under an opt-in build flag
`PHARO_ENABLE_WX_JIT`. Default is unchanged so the shipped JIT is not
broken. When the flag is set, the hooks become functional and the
JIT region is no longer executable until `Make-Executable` is called.
Once Cogit (the Pharo JIT) is audited to call these around every code
emission, the flag can be enabled by default.

```diff
diff --git a/src/memoryUnix.c b/src/memoryUnix.c
index 5899eeec4..b918532d8 100644
--- a/src/memoryUnix.c
+++ b/src/memoryUnix.c
@@ -65,28 +65,35 @@ int mmapErrno = 0;
 void
 sqMakeMemoryExecutableFromTo(unsigned long startAddr, unsigned long endAddr)
 {
-//	sqInt firstPage = roundDownToPage(startAddr);
-//	if (mprotect((void *)firstPage,
-//				 endAddr - firstPage + 1,
-//				 PROT_READ | PROT_WRITE | PROT_EXEC) < 0){
-//		logError("mprotect(x,y,PROT_READ | PROT_WRITE | PROT_EXEC)");
-//		logError("ERRNO: %d\n", errno);
-//		exit(1);
-//	}
+#ifdef PHARO_ENABLE_WX_JIT
+	if (!pageSize) { pageSize = getpagesize(); pageMask = ~(pageSize - 1); }
+	unsigned long firstPage = startAddr & pageMask;
+	if (mprotect((void *)firstPage,
+				 endAddr - firstPage + 1,
+				 PROT_READ | PROT_EXEC) < 0) {
+		logError("mprotect(R|X) on JIT page %p failed (errno %d)",
+				 (void *)firstPage, errno);
+	}
+#else
+	(void)startAddr; (void)endAddr;
+#endif
 }
 
 void
 sqMakeMemoryNotExecutableFromTo(unsigned long startAddr, unsigned long endAddr)
 {
-//	sqInt firstPage = roundDownToPage(startAddr);
-	/* Arguably this is pointless since allocated memory always does include
-	 * write permission.  Annoyingly the mprotect call fails on both linux &
-	 * mac os x.  So make the whole thing a nop.
-	 */
-//	if (mprotect((void *)firstPage,
-//				 endAddr - firstPage + 1,
-//				 PROT_READ | PROT_WRITE) < 0)
-//		logErrorFromErrno("mprotect(x,y,PROT_READ | PROT_WRITE)");
+#ifdef PHARO_ENABLE_WX_JIT
+	if (!pageSize) { pageSize = getpagesize(); pageMask = ~(pageSize - 1); }
+	unsigned long firstPage = startAddr & pageMask;
+	if (mprotect((void *)firstPage,
+				 endAddr - firstPage + 1,
+				 PROT_READ | PROT_WRITE) < 0) {
+		logError("mprotect(R|W) on JIT page %p failed (errno %d)",
+				 (void *)firstPage, errno);
+	}
+#else
+	(void)startAddr; (void)endAddr;
+#endif
 }
 
 
@@ -106,8 +113,13 @@ void* allocateJITMemory(usqInt desiredSize, usqInt desiredPosition){
 
 	logDebug("Trying to allocate JIT memory in %p\n", (void* )desiredBaseAddressAligned);
 
-	if (MAP_FAILED == (result = mmap((void*) desiredBaseAddressAligned, alignedSize, 
-			PROT_READ | PROT_WRITE | PROT_EXEC, 
+#ifdef PHARO_ENABLE_WX_JIT
+	int initialProt = PROT_READ | PROT_WRITE;
+#else
+	int initialProt = PROT_READ | PROT_WRITE | PROT_EXEC;  /* legacy: defeats W^X */
+#endif
+	if (MAP_FAILED == (result = mmap((void*) desiredBaseAddressAligned, alignedSize,
+			initialProt,
 			MAP_FLAGS | additionalFlags, -1, 0))) {
 		logErrorFromErrno("Could not allocate JIT memory");
 		exit(1);
```

## Test plan

- Default build (flag unset): no behavioural change. Existing JIT
  tests pass unchanged. The hooks are now real `mprotect` calls but
  Cogit on this branch never invokes them, so they fire on no
  pages.
- Hardened build (`-DPHARO_ENABLE_WX_JIT=1`): run the JIT test
  suite. Every successful test exercises the hooks; any failures
  indicate code paths in Cogit that do not call the hooks correctly
  and must be fixed before enabling by default.
- Manual: write a Slang/Cogit test that emits and runs a single
  primitive; verify the page is R+X at execution time
  (`/proc/self/maps` on Linux, `vmmap` on macOS).

## Risk notes

- This PR is intentionally conservative. The behavioural change is
  gated behind an off-by-default build flag.
- The hooks now call `mprotect`; they will log warnings on
  unaligned addresses if the JIT ever passes one (existing macOS
  MAP_JIT pages require pthread_jit_write_protect_np instead of
  mprotect — a future follow-up PR should special-case Apple).
- The Windows port already supports W^X (sqWin32SpurAlloc.c:200-227);
  this PR brings Unix to parity for the build that opts in.
- Full W^X requires Cogit to call `sqMakeMemoryNotExecutableFromTo`
  before writing code and `sqMakeMemoryExecutableFromTo` after.
  That work is out of scope for this PR.
