nim-lang / Nim

Nim is a statically typed compiled systems programming language. It combines successful concepts from mature languages like Python, Ada and Modula. Its design focuses on efficiency, expressiveness, and elegance (in that order of priority).
https://nim-lang.org
Other
16.49k stars 1.46k forks source link

Memory corruption on certain code #23797

Open RazrFalcon opened 3 months ago

RazrFalcon commented 3 months ago

Description

Here is an absolute minimal example I was able to come up with:

import std/os
import std/options
import std/streams

type
    Node = ref object
        parent: Option[Node]
        children: seq[Node]

type
    Stream = object
        fstream: StringStream
        parent: Node

let inputData = "\x41\x41\x41\x41\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x42\x42\x42\x42\x00\x00\x00\x00\x43\x43\x43\x43\x0A\x00\x00\x00\x61\x61\x61\x61\x61\x61\x61\x61\x61\x61\x44\x44\x44\x44\x00\x00\x00\x01\x00\x00"

proc newStream(): Stream =
    var root = Node(parent: none(Node), children: @[])
    # we can use FileStream here with the same result
    Stream(fstream: newStringStream(inputData), parent: root)

# Note a global variable here.
var globalStream: Stream = newStream()

proc section(body: proc() {.closure.}) =
    let parentParent = globalStream.parent

    var newParent = Node(parent: some(globalStream.parent), children: @[])
    globalStream.parent.children.add(newParent)
    globalStream.parent = newParent

    body()

    globalStream.parent = parentParent

proc appendChildNode() =
    let node = Node(parent: some(globalStream.parent), children: @[])
    globalStream.parent.children.add(node)

proc readUInt32(): int {.discardable.} =
    let n = globalStream.fstream.readUInt32()
    appendChildNode()
    int(n)

proc readString(length: int): string {.discardable.} =
    let s = globalStream.fstream.readStr(length)
    appendChildNode()
    s

proc readString2(): string {.discardable.} =
    var value = ""
    section() do ():
        let _ = readUInt32() # always 1
        value = readString(0)
        globalStream.fstream.setPosition(globalStream.fstream.getPosition() + 2) # skip 2 bytes

    value

proc readString3(): string {.discardable.} =
    let length = readUInt32()
    if length == 0:
        readString(4)
    else:
        readString(length)

proc parseFile() =
    for i in 0..<2:
        section() do ():
            let tag = readString(4)
            case tag:
            of "BBBB":
                readString2()
                readString3()
            of "AAAA":
                readString2()
                readString3()
                readString3()
                readString3()
            of "CCCC":
                discard readUInt32()
            of "DDDD":
                readString2()
            else:
                raise newException(ValueError, "invalid tag")

echo(commandLineParams()[0]) # required for some reason?!
parseFile()

And run via nimble run -- qwe. The actual value of the first CLI argument doesn't matter, but it has to be present for some reason.

The reason I call it "memory corruption" is that the code crashes in random places. Any changes to the code lead to a crash in a different place or no crash at all. For example, by skipping commandLineParams() call we can avoid a crash. The same by moving of "AAAA": case to on top of of "BBBB":. It's all very random.

The code doesn't make much sense, but it should not matter. It's just a trimmed down version of a real code.

Nim Version

Nim Compiler Version 2.0.8 [MacOSX: arm64]
Compiled at 2024-07-03
Copyright (c) 2006-2023 by Andreas Rumpf

active boot switches: -d:release -d:nimUseLinenoise

Current Output

> nimble run -- qwe
  Verifying dependencies for test_app@0.1.0
   Building test_app/test_app using c backend
ld: warning: ignoring duplicate libraries: '-lm'
qwe
Traceback (most recent call last)
/Users/_/alloc-bug/src/test_app.nim(86) test_app
/Users/_/alloc-bug/src/test_app.nim(67) parseFile
/Users/_/alloc-bug/src/test_app.nim(31) section
/Users/_/alloc-bug/src/test_app.nim(81) :anonymous
/Users/_/alloc-bug/src/test_app.nim(51) readString2
/Users/_/alloc-bug/src/test_app.nim(31) section
/Users/_/alloc-bug/src/test_app.nim(53) :anonymous
/Users/_/alloc-bug/src/test_app.nim(46) readString
/Users/_/alloc-bug/src/test_app.nim(37) appendChildNode
/opt/homebrew/Cellar/nim/2.0.8/nim/lib/system/alloc.nim(1070) realloc
/opt/homebrew/Cellar/nim/2.0.8/nim/lib/system/alloc.nim(1049) alloc
/opt/homebrew/Cellar/nim/2.0.8/nim/lib/system/alloc.nim(861) rawAlloc
SIGSEGV: Illegal storage access. (Attempt to read from nil?)
ringabout commented 3 months ago

It has been fixed on the devel branch