# Pharo VM — local PR collection

Generated:   2026-05-13
Source repo: /Users/wohl/esrc/pharo-vm @ pharo-10 (HEAD 336b3a770)
Audits:      ~/always.md (43 always-fire issues), ~/pharo.md (full audit)

These PRs were prepared as a starting point for fixing the bugs identified
in those two audits. They were NOT submitted to GitHub. Each PR is a
self-contained markdown file with: a citation to the audit entry, a
description of the bug, the fix as a unified diff, a test plan, and risk
notes.

Two directories:

  always/    — PRs for the 43 bugs in ~/always.md (always-fire issues).
               Some PRs combine multiple closely-related fixes in the same
               file (e.g. the SSL hardening trio).
  extended/  — PRs for the bugs from ~/pharo.md that are NOT in always.md.
               These cover edge-case heap-damage, crash bugs, and medium-
               severity hardening that don't fire on every benign run.

A note on AI provenance: the Pharo team is wary of AI-authored PRs.
Before submitting any of these, treat the diff as a draft, re-read the
hunk against current HEAD, run the test plan, and assume responsibility
for the change. Many of these PRs flag risk notes or assumptions that
need a human to confirm before merging.


================================================================
ALWAYS — bugs that fire on every benign run
================================================================

------ memory damage / undefined behaviour ------

  A01-sqNamedPrims-off-by-one.md
       sqNamedPrims.c:56 — strcpy writes strlen+1 into a buffer
       sized by sizeof(struct)+strlen. Defensive +1.
       Severity: HIGH

  A02-callbacks-stack-allocated.md
       ffi callbacks.c:14-33 — CallbackInvocation on stack
       published into runner->callbackStack and global queue.
       Severity: HIGH

  A03-typesPrimitives-setHandler-before-checks.md
       ffi typesPrimitives.c:174-188 — setHandler runs before
       the failure checks; receiver keeps dangling pointer.
       Severity: HIGH

  A04-threadSafeQueue-read-outside-mutex.md
       threadSafeQueue.c:113-137 — queue->first and node->element
       read outside the mutex; UAF on concurrent dequeue.
       Severity: HIGH

  A05-SocketPlugin-destroy-AIO-UAF.md
       SocketPluginImpl.c:1109-1123 — sqSocketDestroy frees pss
       while AIO close-handler is still registered.
       Severity: HIGH

  A06-ffi-readString-unpinned-pointer.md
       ffi/utils.c:43-50 — readString returns un-pinned image
       pointer; GC between caller's strlen and strcpy invalidates it.
       Severity: HIGH

  A07-callbacks-runner-stack-unlocked.md
       ffi callbacks.c:24-29 — callbackStack chain mutated without
       a lock; corrupts on reentrant multi-thread callbacks.
       Severity: MEDIUM

  A08-pathUtilities-null-check-wrong-var.md
       pathUtilities.c:233-237 — null guard tests wrong variable;
       crash on any directory entry without a dot.
       Severity: HIGH

  A09-pathUtilities-empty-string-underflow.md
       pathUtilities.c:163 — first[strlen(first)-1] reads first[-1]
       when first is empty.
       Severity: MEDIUM

  A10-externalPrimitives-module-name-buffer-race.md
       externalPrimitives.c:57,66 — file-static moduleNameBuffer;
       concurrent loads torn the path.
       Severity: MEDIUM

  A11-debugUnix-async-signal-unsafe-handler.md
       debugUnix.c:88-95,122-162 — SIGSEGV handler runs
       non-async-signal-safe code; SA_NODEFER allows re-entry.
       Severity: HIGH-on-fault

  A12-debugUnix-sa-mask-uninitialized.md
       debugUnix.c:123,144,154 — sa_mask uninitialised stack for
       term/sigpipe handler actions.
       Severity: MEDIUM

  A13-debug-strerror_r-uninitialized.md
       debug.c:57-66 — glibc strerror_r return ignored; prints
       uninitialised stack bytes.
       Severity: MEDIUM

  A14-SocketPlugin-recvUDP-lastError-clobber.md
       SocketPluginImpl.c:2494-2496 — UDP recv writes file-static
       lastError, polluting the resolver's error channel.
       Severity: MEDIUM

  A15-aioWin-alias-and-unchecked-malloc.md
       aioWin.c:451-479 — transient heap-interior alias plus
       unchecked malloc and leak on CreateThread failure.
       Severity: MEDIUM

------ security / correctness (not memory damage) ------

  B01-04-sqUnixSSL-tls-hardening.md
       sqUnixSSL.c:89-143 — TLS hardening on Linux/BSD:
       SSL_CTX_set_verify + min protocol TLS1.2 + cipher list
       + SSL_OP_NO_COMPRESSION / RENEGOTIATION.
       Severity: CRITICAL

  B05-07-sqWin32SSL-tls-hardening.md
       sqWin32SSL.c:214-218,269-275,349-353 — restrict to TLS 1.2,
       pass serverName to SChannel for hostname check, stop
       forging peerName from serverName.
       Severity: CRITICAL

  B06-08-sqMacSSL-tls-hardening.md
       sqMacSSL.c:154-201,262-292,327-384 — TLS 1.2 minimum;
       attach SSL policy with serverName before SecTrustEvaluate;
       stop forging peerName from serverName.
       Severity: CRITICAL

  B09-memoryUnix-W-X-defeated.md
       memoryUnix.c:65-90,109-117 — JIT pages mapped RWX
       permanently; sqMakeMemoryExecutable hooks commented out.
       Severity: HIGH

  B10-debug-error-format-string-footgun.md
       debug.c:45 — error(char*) forwards arg as format string.
       Severity: LOW today / LATENT

  B11-ffi-getHandler-type-confusion.md
       ffi typesPrimitives.c:170-172 — getHandler returns first
       slot of any oop with no class tag check; controlled-dispatch
       primitive in libffi.
       Severity: HIGH

------ build / supply chain ------

  B12-Jenkinsfile-curl-bash.md
       Jenkinsfile:~84,~249 — wget|bash over plain HTTP.
       Severity: CRITICAL

  B13-runTests-curl-bash.md
       scripts/runTests.sh:31 — wget|bash from CI on every PR.
       Severity: CRITICAL

  B14-installCygwin-http.md
       scripts/installCygwin.ps1:7-9 — Cygwin installer + mirror
       over plain HTTP.
       Severity: CRITICAL

  B15-cmake-mutable-git-tags.md
       cmake/import{LibFFI,LibGit2,SDL2}.cmake — GIT_TAG to mutable
       tags; pin to commit SHA.
       Severity: HIGH

  B16-cmake-DownloadProject-no-URL_HASH.md
       macros.cmake + cmake/import*.cmake — DownloadProject calls
       without URL_HASH for every third-party tarball.
       Severity: HIGH

  B17-cmake-Freetype-direct-download.md
       cmake/importFreetype2.cmake:47-49 — direct savannah download
       without URL_HASH.
       Severity: HIGH

  B18-docker-unpinned-base-images.md
       docker/{ubuntu-arm64,debian10-armv7}/Dockerfile — base image
       tags not pinned by @sha256.
       Severity: HIGH

  B19-Jenkinsfile-scp-StrictHostKey-no.md
       Jenkinsfile:97-403 — scp -o StrictHostKeyChecking=no for
       all artifact uploads.
       Severity: HIGH

  B20-Jenkinsfile-SIGN_CERT_PASSWORD-env.md
       Jenkinsfile:138 + cmake/sign.cmake:11 — signing passphrase
       in env across the whole build.
       Severity: MEDIUM

  B21-github-workflow-permissions.md
       .github/workflows/continuous-integration-workflow.yaml:2 —
       no permissions block; fork PR has full token scope.
       Severity: HIGH

  B22-github-workflow-EOL-actions.md
       Same file — EOL runners (ubuntu-18.04, windows-2016) and
       EOL action versions (@v1).
       Severity: LOW

  B23-cpack-SHA1-checksum.md
       cmake/packaging.cmake:92 — CPACK_PACKAGE_CHECKSUM "SHA1".
       Severity: LOW

  B24-installCygwin-suppressed-warnings.md
       scripts/installCygwin.ps1:35-48 — cygwin -q suppresses
       signature warnings during install.
       Severity: LOW

  B25-cmake-Linux-hardening-flags.md
       CMakeLists.txt + cmake/Linux.cmake — missing
       -D_FORTIFY_SOURCE=2, -fstack-protector-strong, -fPIE/-pie,
       -Wformat-security, -Wl,-z,relro/now/noexecstack.
       Severity: HIGH

  B26-cmake-silenced-warnings.md
       CMakeLists.txt:206,266-296 — -Wno-int-conversion /
       -Wno-pointer-sign silence real bug classes.
       Severity: MEDIUM

  B27-cmake-Linux-rpath-origin.md
       cmake/Linux.cmake:1 — RPATH set to "." instead of $ORIGIN.
       Severity: MEDIUM

  B28-CMakeLists-Windows-hardening.md
       CMakeLists.txt:206 — Windows builds lack /GS, /guard:cf,
       /DYNAMICBASE, /NXCOMPAT.
       Severity: HIGH


================================================================
EXTENDED — bugs from pharo.md NOT in always.md
================================================================

------ section 2: critical heap damage ------

  2.1-2.2-DSAPrims-missing-return-after-fail.md
       DSAPrims.c:174,304,309 — missing `return 0;` after
       primitiveFailFor in BigDivide and BigMultiply; clean
       image-driven heap-write primitive.
       Severity: CRITICAL

  2.3-JPEGReadWriter2-scanline-bounds.md
       sqJPEGReadWriter2Plugin.c:202-280 + caller — scanline loop
       bounded by JPEG header not Form dimensions; remote attacker
       primitive via malicious JPEG.
       Severity: CRITICAL

  2.4-JPEGReader-blockIndex-OOB.md
       JPEGReaderPlugin.c:436-438,551,573,596 — blockIndex
       unchecked against MaxMCUBlocks; OOB pointer read+deref.
       Severity: CRITICAL

  2.7-FFI-primitiveCopyFromTo-image-size.md
       ffi primitiveUtils.c:14-41 — image-supplied size cast to
       size_t becomes SIZE_MAX-ish; heap obliteration.
       Severity: CRITICAL

------ section 3: high heap damage ------

  3.1-UnixOSProcess-realpath-1024-buffer.md
       UnixOSProcessPlugin.c:4180-4219 — 1024-byte buffer for
       realpath; PATH_MAX is 4096 on Linux.
       Severity: HIGH

  3.2-faSupport-readlink-off-by-one.md
       faSupport.c:480-483 — readlink off-by-one stack overflow.
       Severity: HIGH

  3.3-MiscPrim-decompress-index-validation.md
       MiscPrimitivePlugin.c:425-466 — index<1 + bm format
       missing, OOB read at negative offsets.
       Severity: HIGH

  3.4-MiscPrim-compress-signed-overflow.md
       MiscPrimitivePlugin.c:195-215 — size*4 signed wrap defeats
       destSize check.
       Severity: HIGH

  3.5-BitBlt-depth-zero-divzero-and-overflow.md
       BitBltPlugin.c:3083-3115,3233-3260,5526-5537 — depth==0
       divide-by-zero + signed multiply overflow on size check.
       Severity: HIGH

  3.6-SurfacePlugin-manualSurface-width-depth.md
       sqManualSurface.c:120-167 — width*depth signed overflow +
       setManualSurfacePointer accepts wild pointer.
       Severity: HIGH

  3.7-B2D-primitiveCopyBuffer-GWBufferTop-unchecked.md
       B2DPlugin.c:10770-10781 — GWBufferTop not validated;
       write past dst.
       Severity: HIGH

  3.8-B2D-nSegments-multiply-overflow.md
       B2DPlugin.c:9499-9522,9672-9674,9777,9782 — nSegments*3/6
       signed overflow defeats length check.
       Severity: HIGH

  3.9-externalPrimitives-lookupName-strcpy.md
       externalPrimitives.c:126-138 — strcpy of caller-controlled
       lookupName into 256-byte stack buffer.
       Severity: HIGH

  3.10-utils-setVM-setImage-setVMPath-strcpy.md
       utils.c:173-222 — setVMName/Image/Path strcpy into
       PATH_MAX globals; corrupt adjacent globals.
       Severity: HIGH

  3.11-utilsMac-fillApplicationDirectory-strcpy.md
       utilsMac.mm:8-11 — strcpy of NSBundle path into PATH_MAX
       buffer; deep install path corrupts globals.
       Severity: HIGH

  3.12-winDebugWindow-newline-doubling-overflow.md
       winDebugWindow.c:127,182-193 — newline-doubling output
       overruns logBuffer2 by 1 when L == LOGBUFFER_SIZE.
       Severity: HIGH

  3.13-fileUtilsWin-FILE_NAME_INFO-NUL.md
       fileUtilsWin.c:64 — FileNameLength/sizeof(WCHAR) trailing-
       NUL write lands 1 wchar past the malloc.
       Severity: HIGH

  3.14-sqUnixSSL-unchecked-allocations.md
       sqUnixSSL.c:152-176 — sqCreateSSL unchecked calloc / BIO_new /
       realloc; OOM → SEGV + leak.
       Severity: HIGH

  3.15-sqWin32SSL-extractPeerName-alloca.md
       sqWin32SSL.c:284-295 — alloca() sized by peer cert CN
       length; stack overflow from a large CN.
       Severity: HIGH

  3.16-sqWin32SSL-encrypt-int-overflow.md
       sqWin32SSL.c:746-769 — total = three DWORDs as signed int
       wraps; memcpy past dstBuf.
       Severity: HIGH

  3.17-sqSSL-handle-negative-OOB.md
       sqUnixSSL.c:74-76 + sqWin32SSL.c:96-98 + sqMacSSL.c:116-119 —
       sslFromHandle has no lower bound; negative handle reads
       handleBuf at huge negative offset.
       Severity: HIGH

  3.18-sqUnixSSL-peerName-uninitialized-stack.md
       sqUnixSSL.c:319-330,405-417 — X509_NAME_get_text_by_NID
       return ignored; peerName from stack garbage.
       Severity: HIGH

  3.23-SocketPlugin-stat-non-NUL.md
       SocketPluginImpl.c:1898-1928 — stat() on non-NUL-terminated
       image buffer; OOB read of heap.
       Severity: HIGH

  3.24-SocketPlugin-strncpy-no-NUL.md
       SocketPluginImpl.c:1698 — strncpy of DNS PTR record without
       NUL term; downstream strlen walks off end.
       Severity: HIGH

  3.25-LargeIntegers-digitLshift-shiftCount-overflow.md
       LargeIntegers.c:642-645 — shiftCount overflow on 32-bit
       sqInt → undersized alloc, then cDigitLshift OOB write.
       Severity: HIGH

------ section 4: crash bugs (NULL deref, divide-by-zero) ------

  4.1-client-setImageName-NULL-crash.md
       client.c:222-225 → utils.c:203 — setImageName(NULL) on
       realpath failure; SEGV during init.
       Severity: HIGH

  4.4-B2D-depth-zero-divzero.md
       B2DPlugin.c:9672 — divide-by-zero on Form depth == 0
       (B2D side; BitBlt covered by 3.5).
       Severity: HIGH

  4.5-JPEGReader-partial-zero-scale-divzero.md
       JPEGReaderPlugin.c:431-434 — only one of sx,sy == 0 still
       triggers divide-by-zero.
       Severity: MEDIUM

  4.6-winDebug-fopen-unchecked.md
       winDebug.c:138-167 — crash handler fopen unchecked; SEGV
       inside the crash report when CWD is read-only.
       Severity: MEDIUM

  4.7-aioWin-unchecked-mallocs.md
       aioWin.c:38,498,522 — unchecked allocs; NULL deref on OOM
       under heavy network load.
       Severity: MEDIUM-HIGH

  4.8-FFI-worker-unchecked-mallocs.md
       sameThread.c:35-39, workerTask.c:14,28,36, functionDefinition
       Primitives.c:11-13,29-31, callbackPrimitives.c:35 — many
       unchecked mallocs across the FFI layer.
       Severity: MEDIUM-HIGH

  4.9-sqExternalSemaphores-realloc-NULL.md
       sqExternalSemaphores.c:106-109 — realloc-NULL deref + leak
       + int overflow on sz*sizeof.
       Severity: MEDIUM-HIGH

  4.11-aio-epoll_wait-invalid-fd.md
       extracted/vm/src/unix/aio.c:290-292 — epoll_wait called
       with -1 fd when fillEPollDescriptor fails.
       Severity: MEDIUM

------ section 5: medium severity ------

  5.5-sqWin32SSL-sqAddPfxCertToStore-unchecked.md
       sqWin32SSL.c:903-933 — unchecked CertOpenSystemStore +
       DWORD-as-int arithmetic.
       Severity: MEDIUM

  5.6-sqMacSSL-getPeerCertificates-trust-leak.md
       sqMacSSL.c:298-323 — `trust` CFRef leaked on
       CFArrayCreateMutable failure.
       Severity: MEDIUM

  5.8-imageAccess-sz-count-overflow.md
       imageAccess.c:78,136 — sz*count overflow before fread/fwrite.
       Severity: MEDIUM

  5.9-win32Main-totalSize-overflow.md
       win32Main.c:33-37 — totalSize int overflow on summing UTF-8
       lengths before malloc.
       Severity: MEDIUM

  5.10-sqUnixCharConv-toupper-UB-malloc.md
       sqUnixCharConv.c:210 — toupper(signed char) UB +
       unchecked malloc.
       Severity: MEDIUM

  5.11-LargeIntegers-Montgomery-empty-operands.md
       LargeIntegers.c:2278-2317 — pSecond[0]/pThird[0] read
       without secondLen/thirdLen >= 1 check.
       Severity: MEDIUM

  5.12-UnixOSProcess-cStringFromString.md
       UnixOSProcessPlugin.c:411-422 — unchecked calloc +
       len+1 overflow on huge image string.
       Severity: MEDIUM

  5.13-BitBlt-cmShiftTable-shift-UB.md
       BitBltPlugin.c:987-990,2404-2407 — shift amounts >= 32
       from image-supplied ColorMap → UB.
       Severity: MEDIUM

  5.14-JPEG-huffman-shift-UB.md
       JPEGReaderPlugin.c:738,798 — 1U << byte where byte >= 32
       is UB.
       Severity: MEDIUM

  5.15-SocketPlugin-startIndex-count-wraps.md
       SocketPlugin.c:1795,1858,2060 — (startIndex+count)-1
       signed wrap defeats slotSizeOf bound.
       Severity: MEDIUM

  5.16-SocketPlugin-findOption-strncpy.md
       SocketPluginImpl.c:1534-1547 — strncpy non-NUL-term
       footgun.
       Severity: MEDIUM

  5.18-FilePlugin-mac-strncpy-lastPath.md
       sqUnixFile.c:143 — strncpy(lastPath, …, MAXPATHLEN)
       missing NUL term.
       Severity: MEDIUM

  5.19-FileAttributes-access-lstat-TOCTOU.md
       faSupport.c:435-448 — access()/lstat() vs open() TOCTOU
       race.
       Severity: MEDIUM

  5.20-macAlias-FSRefMakePath-PATH_MAX.md
       macAlias.c:41 — FSRefMakePath hard-codes PATH_MAX,
       ignores caller bound.
       Severity: MEDIUM

  5.21-LocalePlugin-currency-symbol-TOCTOU.md
       LocalePlugin.c:160-162 — currency-symbol two-call TOCTOU.
       Severity: MEDIUM

  5.22-LocalePlugin-sqMacSSL-vsprintf-stack.md
       sqMacSSL.c:80-112 (and LocalePlugin) — vsprintf into fixed
       1024-byte stack buffer.
       Severity: MEDIUM

  5.23-SocketPlugin-nameToAddr-AF_INET6.md
       SocketPluginImpl.c:368-402 — nameToAddr drops AF_INET6
       results silently.
       Severity: MEDIUM

  5.25-parameters-chdir-unsanitized.md
       parameters/parameters.c:574 — chdir(originalArgument) with
       no validation.
       Severity: LOW

  5.26-FFI-executeWorkerTask-leak.md
       ffi worker.c:210-219 — WorkerTask never freed after
       execution.
       Severity: LOW

------ section 6: TLS configuration recap ------

  6.4-sqUnixSSL-X509_check_host-strnlen.md
       sqUnixSSL.c:309-323 — serverName truncated by strnlen;
       attacker-influenced length drives checked-vs-actual
       mismatch.
       Severity: MEDIUM


================================================================
HOW TO USE THESE PRs
================================================================

Each PR file contains a unified diff inside a ```diff … ``` block.
To apply one against the local checkout:

    cd ~/esrc/pharo-vm
    git apply --check ../PR/always/A01-sqNamedPrims-off-by-one.md
    # if --check succeeds, apply for real:
    git apply ../PR/always/A01-sqNamedPrims-off-by-one.md

`git apply` is permissive about surrounding context, but each diff
should be re-read against current HEAD before being applied — the
audit was taken on HEAD 336b3a770 and HEAD may have moved since.

Before any of these reaches GitHub, the work should be:

  1. Reviewed by a human who understands the affected subsystem.
  2. Built and tested locally (each PR includes a test plan).
  3. Re-described in the PR title/body in the Pharo project's own
     style — the markdown in these files is for the human reviewer
     and is not a drop-in PR body.

The audit deliberately overstates some items (every "always-fire"
claim was spot-checked, but the patches in this collection are
defensive — they harden the code even where the bug was
padding-dependent or required a specific build flag).
