Skip to content

repair in-place marking of an empty continuation stack - #1096

Merged
mflatt merged 2 commits into
cisco:mainfrom
jdek:fix-mark-zero-length-stack
Nov 7, 2026
Merged

mflatt merged 2 commits into
cisco:mainfrom
jdek:fix-mark-zero-length-stack

Conversation

@jdek

@jdek jdek commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

Debugging this with an agent this appears correct, but I don't know enough about the collector to be sure.

;; empty-k's frame is larger than the 128-byte underflow limit, so when
;; the first call/cc returns, the stack is split with that frame alone at
;; the base of the current stack, above a link made by the split (whose
;; attachments are #f). The second call/cc, in tail position, then
;; reifies an empty stack: k0 has length 0 and its stack pointer X is the
;; base of the current stack. The collection just before copied that
;; stack into the oldest generation, where the next full collection marks
;; in place. kq is then captured 10000 frames deeper on the same stack,
;; so it also starts at X, and its frames run over several segments.
;;
;; In the next full collection k0 is swept before kq. copy_stack marks a
;; zero-length object at X: the mark bit for X is set, but no bytes are
;; marked. When kq is swept, X is already marked, so kq's stack is
;; skipped. The segments that only kq's frames occupy are not marked and
;; are freed. The junk allocation reuses them, and the following
;; collection sweeps kq's frames from that memory.

(define (deep n th) (if (fx= n 0) (th) (car (list (deep (fx- n 1) th)))))

(define empty-k
  (lambda (a b c d e f g h i j k l m n o p q)
    (collect (collect-maximum-generation))
    (call/cc (lambda (k) k))
    (if (fx= 0 a b c d e f g h i j k l m n o p q) (call/cc (lambda (k) k)) #f)))

(define (trial)
  ;; deep 50000: leave the boot stacks; collect: empty the stack cache,
  ;; so that call/1cc runs its body on a fresh stack
  (let ([ks (deep 50000
              (lambda ()
                (collect)
                (call/1cc
                  (lambda (escape)
                    (let ([k0 (empty-k 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)])
                      (deep 10000 (lambda () (call/cc (lambda (k) (escape (list k0 k)))))))))))])
    (collect (collect-maximum-generation))
    (let ([junk (make-list 100000 (cons 1 2))])
      (collect (collect-maximum-generation))
      (and (= (length junk) 100000) (= (length ks) 2)))))

(do ([i 0 (+ i 1)]) ((= i 4)) (trial))
(display "ok\n")

Run:

$ scheme --script t.ss
nonrecoverable invalid memory reference
zsh: abort      scheme --script t.ss

@mflatt
mflatt force-pushed the fix-mark-zero-length-stack branch from a7ba767 to 7b243f8 Compare November 7, 2026 02:54
@mflatt

mflatt commented Nov 7, 2026

Copy link
Copy Markdown
Contributor

Thanks! The explanation and repair does sound right to me. I added the example as a test and simplified the release note.

@mflatt
mflatt merged commit ed9bbe7 into cisco:main Nov 7, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants