Skip to content

interp: do not write a pointer across an element of a typed global - #5912

Draft
davecheney wants to merge 1 commit into
tinygo-org:devfrom
davecheney:dfc/interp-writeback-pointer-words
Draft

davecheney wants to merge 1 commit into
tinygo-org:devfrom
davecheney:dfc/interp-writeback-pointer-words

Conversation

@davecheney

Copy link
Copy Markdown
Contributor

Interp keeps a pointer in memory as a series of equal pointer words. When interp writes a changed global back to LLVM, rawValue.toLLVMValue examines only the first word of each scalar element. If a program stores a whole pointer at a position that is not a pointer or uintptr element of the global type, the result is not correct:

  • If the element starts with bytes, interp writes the pointer bytes as zero. rawValue.Uint sets r.interpErr, but the writeback does not examine it.
  • If the element starts with a pointer word, interp writes the full pointer into the element, also when the element holds only a part of it.
  • If the element is in a byte array, RunFunc stops the build with the error "ptrtoint integer size does not equal pointer size".

An LLVM initializer cannot hold a part of a pointer in an integer or float element. LLVM 18 and later do not have the shl, lshr, and, or constant expressions.

Now interp does the store at runtime when the LLVM global type has no pointer or uintptr element at the position of a stored pointer. The memcpy and memmove case does the same check for the destination. The check uses the element layout and not the alignment, because a pointer can be at an odd offset on AVR. Interp still writes a pointer at a correct element position in the initializer. This check does not change heap objects.

Also, rawValue.toLLVMValue now returns errInvalidPtrToIntSize for a scalar element that cannot hold its pointer words. Thus the writeback does not write pointer bytes as zero, and it does not set r.interpErr.

This program prints "global: true" with Go. It prints "global: false" with TinyGo dev and with TinyGo v0.43.0:

package main

import "unsafe"

var x int
var g [2]uintptr

func store(p unsafe.Pointer) {
	*(*uintptr)(unsafe.Add(p, unsafe.Sizeof(uintptr(0))/2)) = uintptr(unsafe.Pointer(&x))
}

func init() { store(unsafe.Pointer(&g)) }

//go:noinline
func rt() (v [2]uintptr) { store(unsafe.Pointer(&v)); return }

func main() {
	println("global:", g == rt())
}

Tests:

  • interp/testdata/writeback.ll stores and copies a pointer into a [2 x i64] at offset 4, into a double and into a byte array. These stores stay at runtime. Interp still writes a pointer in an i64 element, in a packed struct at offset 1 and in a ptr element into the initializer.
  • testdata/init_writeback.go does the same stores in init and at runtime, and compares the results. The output does not change with the target. Without this change, the build stops with "ptrtoint integer size does not equal pointer size". If the byte array case is removed, the other cases print false.

Interp keeps a pointer in memory as a series of equal pointer words. When
interp writes a changed global back to LLVM, rawValue.toLLVMValue examines
only the first word of each scalar element. If a program stores a whole
pointer at a position that is not a pointer or uintptr element of the global
type, the result is not correct:

- If the element starts with bytes, interp writes the pointer bytes as zero.
  rawValue.Uint sets r.interpErr, but the writeback does not examine it.
- If the element starts with a pointer word, interp writes the full pointer
  into the element, also when the element holds only a part of it.
- If the element is in a byte array, RunFunc stops the build with the error
  "ptrtoint integer size does not equal pointer size".

An LLVM initializer cannot hold a part of a pointer in an integer or float
element. LLVM 18 and later do not have the shl, lshr, and, or constant
expressions.

Now interp does the store at runtime when the LLVM global type has no pointer
or uintptr element at the position of a stored pointer. The memcpy and
memmove case does the same check for the destination. The check uses the
element layout and not the alignment, because a pointer can be at an odd
offset on AVR. Interp still writes a pointer at a correct element position in
the initializer. This check does not change heap objects.

Also, rawValue.toLLVMValue now returns errInvalidPtrToIntSize for a scalar
element that cannot hold its pointer words. Thus the writeback does not write
pointer bytes as zero, and it does not set r.interpErr.

This program prints "global: true" with Go. It prints "global: false" with
TinyGo dev and with TinyGo v0.43.0:

    package main

    import "unsafe"

    var x int
    var g [2]uintptr

    func store(p unsafe.Pointer) {
    	*(*uintptr)(unsafe.Add(p, unsafe.Sizeof(uintptr(0))/2)) = uintptr(unsafe.Pointer(&x))
    }

    func init() { store(unsafe.Pointer(&g)) }

    //go:noinline
    func rt() (v [2]uintptr) { store(unsafe.Pointer(&v)); return }

    func main() {
    	println("global:", g == rt())
    }

Tests:

- interp/testdata/writeback.ll stores and copies a pointer into a [2 x i64]
  at offset 4, into a double and into a byte array. These stores stay at
  runtime. Interp still writes a pointer in an i64 element, in a packed struct
  at offset 1 and in a ptr element into the initializer.
- testdata/init_writeback.go does the same stores in init and at runtime, and
  compares the results. The output does not change with the target. Without
  this change, the build stops with "ptrtoint integer size does not equal
  pointer size". If the byte array case is removed, the other cases print
  false.
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.

1 participant