GNU bug report logs - #79762
Guile may run stale code if source changes too quickly

Previous Next

Package: guile;

Reported by: Rob Browning <rlb <at> defaultvalue.org>

Date: Mon, 3 Nov 2025 22:02:01 UTC

Severity: normal

To reply to this bug, email your comments to 79762 AT debbugs.gnu.org.

Toggle the display of automated, internal messages from the tracker.

View this report as an mbox folder, status mbox, maintainer mbox


Report forwarded to bug-guile <at> gnu.org:
bug#79762; Package guile. (Mon, 03 Nov 2025 22:02:01 GMT) Full text and rfc822 format available.

Acknowledgement sent to Rob Browning <rlb <at> defaultvalue.org>:
New bug report received and forwarded. Copy sent to bug-guile <at> gnu.org. (Mon, 03 Nov 2025 22:02:02 GMT) Full text and rfc822 format available.

Message #5 received at submit <at> debbugs.gnu.org (full text, mbox):

From: Rob Browning <rlb <at> defaultvalue.org>
To: bug-guile <at> gnu.org
Subject: Guile may run stale code if source changes too quickly
Date: Mon, 03 Nov 2025 16:01:17 -0600
[Message part 1 (text/plain, inline)]
In the past I'd noticed that guile appeared to be running stale code,
and I finally got a chance to track it down with help from Dale Smith.

The cause is that filesystems have finite (and sometimes quite coarse)
timestamp "granularity", and guile currently treats equal timestamps
between a .scm file and .go file as meaning that the .go file is up to
date, i.e. represents the compilation of the code in the .scm
file. Right now that is not always true, and can produce undesiarable
results:

For example, if you do this:

  echo '(display "something bad\n")' > test.scm
  guile test.scm
  echo '(display "the (broken) fix)' > test.scm
  guile test.scm

The final guile invocation may actually succeed while printing
"something bad" if the test.go generated by the first guile invocation
ends up with the same timestamp as the one with the fix. It's not even
relevant that the fix is actually itself broken (invalid syntax) because
guile never attempts to compile it --- it just runs the "bad" test.go.

To fix this, we can change compiled_is_fresh in load.c to require the
.go to be strictly newer than the corresponding .scm, i.e. change the <=
to < here:

  if (source_mtime.tv_sec < compiled_mtime.tv_sec
      || (source_mtime.tv_sec == compiled_mtime.tv_sec
          && source_mtime.tv_nsec <= compiled_mtime.tv_nsec))
    compiled_is_newer = 1;
  ...

and in more-recent? in boot-9.scm, change >= to > here:

        (and (= (stat:mtime stat1) (stat:mtime stat2))
             (>= (stat:mtimensec stat1)
                 (stat:mtimensec stat2)))))

This script demonstrates the problem for the latter case:

[test-stale-load (text/x-sh, inline)]
#!/bin/bash

set -ueo pipefail

guile="${GUILE:-guile}"
top="$(pwd)"

tmpdir=''
on-exit() { cd "$top"; if test "$tmpdir"; then rm -rf "$tmpdir"; fi; }
trap on-exit EXIT

tmpdir="$(mktemp -d show-stale-load-XXXXXXX)"
cd "$tmpdir"
mkdir cache
export XDG_CACHE_HOME="$(pwd)/cache"

while true; do
    echo '(display "game over\n")' > foo.scm
    "$guile" foo.scm > /dev/null

    # (Compilation of) this broken fix should be rejected, but rarely
    # it's not because foo.scm here ends up with the same timestamp as
    # the foo.go compiled above.
    echo '(display "fixed)' > foo.scm
    if "$guile" foo.scm | tee foo.log; then
        echo 'broken fix did not provoke an error'
    fi
    if grep -q 'game over' foo.log; then
        # stat foo.scm "$(find -name foo.scm.go)"
        echo '=== ran bad code even after fix was in place ==='
        exit 2
    fi
done
[Message part 3 (text/plain, inline)]
Thanks to Dale Smith for finding the root causes.
-- 
Rob Browning
rlb @defaultvalue.org and @debian.org
GPG as of 2011-07-10 E6A9 DA3C C9FD 1FF8 C676 D2C4 C0F0 39E9 ED1B 597A
GPG as of 2002-11-03 14DD 432F AE39 534D B592 F9A0 25C8 D377 8C7E 73A4

This bug report was last modified 1 day ago.

Previous Next


GNU bug tracking system
Copyright (C) 1999 Darren O. Benham, 1997,2003 nCipher Corporation Ltd, 1994-97 Ian Jackson.