Skip to content

Runtime segmentation fault using LTO, ORC, and -d:release with one {.localPassC: "-fno-lto".} #25001

Open
@tersec

Description

@tersec

Nim Version

Nim Compiler Version 2.2.4 [Linux: amd64]
Compiled at 2025-06-08
Copyright (c) 2006-2025 by Andreas Rumpf

git hash: f7145dd26efeeeb6eeae6fff649db244d81b212d
active boot switches: -d:release
Nim Compiler Version 2.2.5 [Linux: amd64]
Compiled at 2025-06-15
Copyright (c) 2006-2025 by Andreas Rumpf

git hash: 7fdbdb2f20a9d43e62afac8c32dfd3a3e39d3149
active boot switches: -d:release
Nim Compiler Version 2.3.1 [Linux: amd64]
Compiled at 2025-06-15
Copyright (c) 2006-2025 by Andreas Rumpf

git hash: 7701b3c7e6f6c640a89cc445b40f466834ab4fcf
active boot switches: -d:release

On Debian unstable with:

gcc (Debian 14.2.0-19) 14.2.0
Copyright (C) 2024 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Description

This is a somewhat involved one, but I wasn't able to get it under 4 distinct modules without losing the 100% reliability it has for me:

$ wc -l *.nim
  20 e.nim
   7 o.nim
  26 t.nim
  89 w.nim
 142 total

Notably, exactly one of the files signals that it should not be using LTO. This reflects the actual use case, where it's important to be able to do this.

The main module, w.nim:

import std/[macros, tables], ./o

proc y(): seq[seq[byte]] =
  result = @[@[248'u8, 177, 160, 129, 136, 149, 159, 31, 252, 215, 147, 250, 28, 74, 127, 243, 250, 52, 43, 117, 253, 206, 185, 136, 179, 23, 70, 75, 37, 169, 40, 81, 139, 29, 85, 128, 160, 166, 92, 64, 107, 103, 166, 196, 94, 147, 183, 129, 212, 225, 123, 145, 5, 105, 226, 248, 243, 193, 9, 179, 25, 169, 168, 252, 112, 223, 115, 37, 41, 128, 160, 212, 49, 8, 53, 235, 82, 204, 21, 4, 254, 38, 152, 121, 245, 19, 127, 137, 243, 84, 79, 146, 233, 16, 10, 222, 19, 147, 71, 196, 38, 5, 6, 128, 128, 128, 128, 128, 160, 194, 171, 71, 247, 21, 130, 2, 59, 51, 27, 110, 162, 104, 73, 163, 174, 229, 43, 72, 28, 43, 246, 103, 5, 27, 137, 130, 21, 106, 1, 201, 49, 128, 128, 128, 128, 160, 198, 39, 225, 154, 149, 227, 112, 175, 149, 233, 24, 177, 216, 49, 194, 32, 227, 116, 223, 82, 202, 202, 87, 37, 129, 92, 198, 14, 198, 134, 161, 216, 128], @[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]
import ./t

type B = ref object
  v: B

proc x(t: var B, g: openArray[byte]) =
  let g = @g
  while t != nil:
    discard getOrDefault(default(Table[seq[byte], int]), g)
    t = t.v

macro a(): untyped =
  result = newStmtList()
  for _, _ in [""]: discard

type K = object
  case kind: range[0 .. 2]
  of 2: discard
  of 1:
    e: array[32, byte]
    r: uint8
  of 0: p: seq[byte]

proc s(f: L): Q[L] = Q[L](g: true, z: h(f))
proc n(d: L): Q[seq[byte]] =
  try:
    Q[seq[byte]](g: true, z: @(d.bytes.p(item(d).payload)))
  except CatchableError:
    Q[seq[byte]](g: false)

template k(s: untyped): untyped =
  var result = newSeq[byte](len(s))
  discard s[0]
  result

template c[T](self: Q[T]): auto =
  let w = self
  case w.g
  of false:
    result = Q[K]()
    return
  of true:
    w.z

proc m(w: L): Q[seq[byte]] = Q[seq[byte]](g: true, z: k(toOpenArray(w.bytes, w.position, 1)))
proc w(): array[32, byte] = discard
proc p(f: L, i: uint8): Q[K] =
  var w = f
  while true:
    if i == 0:
      discard n(c(s(w)))
      return Q[K](g: true, z: K(kind: 0))
    else:
      let h = c(s(w))
      if j(h):
        if len(c(m(h))) > 32:
          return Q[K]()
        else:
          w = h
      else:
        let d = len(c(n(h)))
        if d == 32:
          discard w()
          return Q[K](g: true, z: K(kind: 1))
        elif d == 0:
          return Q[K](g: true, z: K(kind: 2))
        return Q[K](g: false)

proc u(data: openArray[byte]): L = L(bytes: @data, position: 0)
proc v(g: seq[byte]) =
  var e = 64'u8
  while true:
    var t = new B
    x(t, [])
    let node = g
    let next = p(u(node), e).z
    case next.kind
    of 0, 2:
      return
    of 1:
      e = next.r

discard {"": 0}.toTable()
v(y()[0])
import ./e

e.nim:

import std/[os, sets]

type W = ref object
var s: seq[W]
var h: HashSet[string]

proc f(c: openArray[string]) =
  var g: string
  let _ = 0
  for d in c:
    if d == "   " or d == "   ":
      g = d[3..^1]
    else:
      h.incl(d)
  s.add(W())

proc w(): seq[string] =
  for i in 1..paramCount():
    result.add(paramStr(i))
f(w())

o.nim:

{.localPassC: "-fno-lto".}
type Q*[T] = object
  case g*: bool
  of false:
    discard
  of true:
    z*: T

t.nim:

type
  L* = object
    bytes*: seq[byte]
    position*: int
  D = tuple[payload: Slice[int], _: int]

template p*(n: openArray[byte], s: Slice[int]): openArray[byte] =
  if s.b >= len(n): raiseAssert ""
  toOpenArray(n, s.a, s.b)

proc k(f: openArray[byte], t = 0): D =
  if f[t] <= 0x7f:
    raiseAssert ""
  elif f[t] <= 0xb7:
    (1 .. int(f[t] - 128), 0)
  else:
    (2 .. 178, 0)

proc item*(e: L): D = k(e.bytes, e.position)
proc j*(v: L): bool = v.bytes[v.position] >= byte(0xc0)
proc h*(x: L): L =
  let m = item(x)
  var p = k(x.bytes.p(m.payload.a .. m.payload.b)).payload
  for _ in 0 ..< 1:
    p = k(x.bytes.p(2 .. 178)).payload
  L(bytes: @(x.bytes.p(2 .. 2 + p.b)), position: 0)

As a helper script to ensure the reliability of the reproduction:

#!/bin/bash
set -u

# Check if a command is provided
if [ -z "$1" ]; then
    echo "Usage: $0 <command>"
    exit 1
fi

COMMAND="$1"  # The command to run
SUCCESS_COUNT=0
TOTAL_RUNS=3000

for ((i=1; i<=TOTAL_RUNS; i++)); do
    # Run the command and redirect both stdout and stderr to /dev/null
    { $COMMAND; } > /dev/null 2>&1
    EXIT_CODE=$?
    
    if [ $EXIT_CODE -eq 0 ]; then
        ((SUCCESS_COUNT++))
    fi
done

# Print the summary of SUCCESS_COUNT
echo "$COMMAND SUCCESS_COUNT: $SUCCESS_COUNT"

if [ $SUCCESS_COUNT -gt 100 ]; then
    exit 1
else
    exit 0
fi

to ensure it consistently does not run successfully in ORC/-d:release/LTO.

The tests so far:

  • doesn't depend on --threads:on or --threads:off, crashes similarly.
  • doesn't crash with refc in any version, only ORC
  • doesn't crash without LTO, or if using LTO on all modules (remove the no-lto pragma)
  • doesn't crash without -d:release

status-im/nim-unittest2#55 (comment) also documents some of this.

Current Output

$ while true; do ~/nim22/bin/nim c -o:$(mktemp) --nimcache:$(mktemp -d) -r w.nim && ~/nim22/bin/nim c -o:$(mktemp) --nimcache:$(mktemp -d) --mm:refc -d:release -r w.nim && ~/nim23/bin/nim c -o:$(mktemp) -r w.nim && ~/nim23/bin/nim c --hints:on --warnings:on -o:/tmp/n23t1 --nimcache:$(mktemp -d) -d:release w.nim && crashy /tmp/n23t1 && ~/nim22/bin/nim c -o:/tmp/n22t0 --nimcache:$(mktemp -d) -d:release --threads:off w.nim && crashy /tmp/n22t0 && ~/nim22/bin/nim c --o:/tmp/n22t1 -d:release --threads:on --nimcache:$(mktemp -d) w.nim && crashy /tmp/n22t1 && ~/nim23/bin/nim c -o:/tmp/n23t0 --nimcache:$(mktemp -d) -d:release --threads:off w.nim && crashy /tmp/n23t0; sleep 4; done
lto-wrapper: warning: using serial compilation of 2 LTRANS jobs
lto-wrapper: note: see the ‘-flto’ option documentation for more information
nim-unittest2/w.nim(16, 7) Hint: 'a' is declared but not used [XDeclaredButNotUsed]
nim-unittest2/w.nim(89, 8) Warning: imported and not used: 'e' [UnusedImport]
lto-wrapper: warning: using serial compilation of 2 LTRANS jobs
lto-wrapper: note: see the ‘-flto’ option documentation for more information
/tmp/n23t1 SUCCESS_COUNT: 0
lto-wrapper: warning: using serial compilation of 2 LTRANS jobs
lto-wrapper: note: see the ‘-flto’ option documentation for more information
/tmp/n22t0 SUCCESS_COUNT: 0
lto-wrapper: warning: using serial compilation of 2 LTRANS jobs
lto-wrapper: note: see the ‘-flto’ option documentation for more information
/tmp/n22t1 SUCCESS_COUNT: 0
lto-wrapper: warning: using serial compilation of 2 LTRANS jobs
lto-wrapper: note: see the ‘-flto’ option documentation for more information
/tmp/n23t0 SUCCESS_COUNT: 0
lto-wrapper: warning: using serial compilation of 2 LTRANS jobs
lto-wrapper: note: see the ‘-flto’ option documentation for more information

Or individually:
$ /tmp/n22t0
SIGSEGV: Illegal storage access. (Attempt to read from nil?)
Segmentation fault
$ /tmp/n22t1
SIGSEGV: Illegal storage access. (Attempt to read from nil?)
Segmentation fault
$ /tmp/n23t0
SIGSEGV: Illegal storage access. (Attempt to read from nil?)
Segmentation fault
$ /tmp/n23t1
SIGSEGV: Illegal storage access. (Attempt to read from nil?)
Segmentation fault

Or Valgrind:
$ valgrind /tmp/n22t0
==845673== Memcheck, a memory error detector
==845673== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==845673== Using Valgrind-3.24.0 and LibVEX; rerun with -h for copyright info
==845673== Command: /tmp/n22t0
==845673== 
==845673== Conditional jump or move depends on uninitialised value(s)
==845673==    at 0x10AAC3: eqdestroy___w_u42 (in /tmp/n22t0)
==845673==    by 0x11429E: v__w_u1355 (in /tmp/n22t0)
==845673==    by 0x109FD2: main (in /tmp/n22t0)
==845673== 
==845673== Use of uninitialised value of size 8
==845673==    at 0x10AAC5: eqdestroy___w_u42 (in /tmp/n22t0)
==845673==    by 0x11429E: v__w_u1355 (in /tmp/n22t0)
==845673==    by 0x109FD2: main (in /tmp/n22t0)
==845673== 
==845673== Invalid read of size 1
==845673==    at 0x10AAC5: eqdestroy___w_u42 (in /tmp/n22t0)
==845673==    by 0x11429E: v__w_u1355 (in /tmp/n22t0)
==845673==    by 0x109FD2: main (in /tmp/n22t0)
==845673==  Address 0x2841f61 is not stack'd, malloc'd or (recently) free'd
==845673== 
SIGSEGV: Illegal storage access. (Attempt to read from nil?)
==845673== 
==845673== Process terminating with default action of signal 11 (SIGSEGV)
==845673==    at 0x48FE95C: __pthread_kill_implementation (pthread_kill.c:44)
==845673==    by 0x48A9CC1: raise (raise.c:26)
==845673==    by 0x48A9DEF: ??? (in /usr/lib/x86_64-linux-gnu/libc.so.6)
==845673==    by 0x10AAC4: eqdestroy___w_u42 (in /tmp/n22t0)
==845673==    by 0x11429E: v__w_u1355 (in /tmp/n22t0)
==845673==    by 0x109FD2: main (in /tmp/n22t0)
==845673== 
==845673== HEAP SUMMARY:
==845673==     in use at exit: 0 bytes in 0 blocks
==845673==   total heap usage: 0 allocs, 0 frees, 0 bytes allocated
==845673== 
==845673== All heap blocks were freed -- no leaks are possible
==845673== 
==845673== Use --track-origins=yes to see where uninitialised values come from
==845673== For lists of detected and suppressed errors, rerun with: -s
==845673== ERROR SUMMARY: 3 errors from 3 contexts (suppressed: 0 from 0)
Segmentation fault

$ valgrind /tmp/n23t1
==845777== Memcheck, a memory error detector
==845777== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==845777== Using Valgrind-3.24.0 and LibVEX; rerun with -h for copyright info
==845777== Command: /tmp/n23t1
==845777== 
==845777== Conditional jump or move depends on uninitialised value(s)
==845777==    at 0x10B643: eqdestroy___w_u42 (in /tmp/n23t1)
==845777==    by 0x114EB6: v__w_u1355 (in /tmp/n23t1)
==845777==    by 0x10A455: main (in /tmp/n23t1)
==845777== 
==845777== Use of uninitialised value of size 8
==845777==    at 0x10B645: eqdestroy___w_u42 (in /tmp/n23t1)
==845777==    by 0x114EB6: v__w_u1355 (in /tmp/n23t1)
==845777==    by 0x10A455: main (in /tmp/n23t1)
==845777== 
==845777== Invalid read of size 1
==845777==    at 0x10B645: eqdestroy___w_u42 (in /tmp/n23t1)
==845777==    by 0x114EB6: v__w_u1355 (in /tmp/n23t1)
==845777==    by 0x10A455: main (in /tmp/n23t1)
==845777==  Address 0x2841f61 is not stack'd, malloc'd or (recently) free'd
==845777== 
SIGSEGV: Illegal storage access. (Attempt to read from nil?)
==845777== 
==845777== Process terminating with default action of signal 11 (SIGSEGV)
==845777==    at 0x48FE95C: __pthread_kill_implementation (pthread_kill.c:44)
==845777==    by 0x48A9CC1: raise (raise.c:26)
==845777==    by 0x48A9DEF: ??? (in /usr/lib/x86_64-linux-gnu/libc.so.6)
==845777==    by 0x10B644: eqdestroy___w_u42 (in /tmp/n23t1)
==845777==    by 0x114EB6: v__w_u1355 (in /tmp/n23t1)
==845777==    by 0x10A455: main (in /tmp/n23t1)
==845777== 
==845777== HEAP SUMMARY:
==845777==     in use at exit: 0 bytes in 0 blocks
==845777==   total heap usage: 0 allocs, 0 frees, 0 bytes allocated
==845777== 
==845777== All heap blocks were freed -- no leaks are possible
==845777== 
==845777== Use --track-origins=yes to see where uninitialised values come from
==845777== For lists of detected and suppressed errors, rerun with: -s
==845777== ERROR SUMMARY: 3 errors from 3 contexts (suppressed: 0 from 0)
Segmentation fault

$ valgrind /tmp/n23t0
==845811== Memcheck, a memory error detector
==845811== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==845811== Using Valgrind-3.24.0 and LibVEX; rerun with -h for copyright info
==845811== Command: /tmp/n23t0
==845811== 
==845811== Conditional jump or move depends on uninitialised value(s)
==845811==    at 0x10B603: eqdestroy___w_u42 (in /tmp/n23t0)
==845811==    by 0x114DDE: v__w_u1355 (in /tmp/n23t0)
==845811==    by 0x10A471: main (in /tmp/n23t0)
==845811== 
==845811== Use of uninitialised value of size 8
==845811==    at 0x10B605: eqdestroy___w_u42 (in /tmp/n23t0)
==845811==    by 0x114DDE: v__w_u1355 (in /tmp/n23t0)
==845811==    by 0x10A471: main (in /tmp/n23t0)
==845811== 
==845811== Invalid read of size 1
==845811==    at 0x10B605: eqdestroy___w_u42 (in /tmp/n23t0)
==845811==    by 0x114DDE: v__w_u1355 (in /tmp/n23t0)
==845811==    by 0x10A471: main (in /tmp/n23t0)
==845811==  Address 0x2841f61 is not stack'd, malloc'd or (recently) free'd
==845811== 
SIGSEGV: Illegal storage access. (Attempt to read from nil?)
==845811== 
==845811== Process terminating with default action of signal 11 (SIGSEGV)
==845811==    at 0x48FE95C: __pthread_kill_implementation (pthread_kill.c:44)
==845811==    by 0x48A9CC1: raise (raise.c:26)
==845811==    by 0x48A9DEF: ??? (in /usr/lib/x86_64-linux-gnu/libc.so.6)
==845811==    by 0x10B604: eqdestroy___w_u42 (in /tmp/n23t0)
==845811==    by 0x114DDE: v__w_u1355 (in /tmp/n23t0)
==845811==    by 0x10A471: main (in /tmp/n23t0)
==845811== 
==845811== HEAP SUMMARY:
==845811==     in use at exit: 0 bytes in 0 blocks
==845811==   total heap usage: 0 allocs, 0 frees, 0 bytes allocated
==845811== 
==845811== All heap blocks were freed -- no leaks are possible
==845811== 
==845811== Use --track-origins=yes to see where uninitialised values come from
==845811== For lists of detected and suppressed errors, rerun with: -s
==845811== ERROR SUMMARY: 3 errors from 3 contexts (suppressed: 0 from 0)
Segmentation fault

Expected Output

No segmentation fault

Known Workarounds

No response

Additional Information

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions