Mixing in Swift into C embedded project

I think you are showing it right, the object is in r0. The zero word is strange, I expected to see a pointer there and it doesn't look like it. Could you dump a memory of the newly created object in a similar manner?

Is CFGetRetainCount() available on that platform?

For some reason I have only this constructor/destructor pair for my class CDontMangleMe
$s4game16startSwiftEngineyyF13CDontMangleMeL_CADycfc build/game.o
$s4game16startSwiftEngineyyF13CDontMangleMeL_CfD build/game.o
in my map file. And based on this fc stands for non-allocating constructor
But I've definitely seen fC before. I guess I need to modify my swift code somehow to make it appear.

And unfortunately debugger does not break on $s4game16startSwiftEngineyyF13CDontMangleMeL_CADycfc for some reason.

Edit: It looks suspicious already :man_detective: - fc and fD ?

Looks like I can't import Foundation, or at least I don't know how to.

Here's some memory dumps. It's from ss17swift_allocObject8metadata12requiredSize0E13AlignmentMaskSpys04HeapC0VGSpys13ClassMetadataVG_S2itF

looks like r6 and r7 contain references to objects
I've dumped memory below


Edit: On the second though it doesn't look like that's what we are looking for, sorry

Nay, r6 & r7 are on the stack.

I'd put a breakpoint (if possible) on the highlighted line and dump r0.
That's where the memory was just allocated and RC set to 1.

Here it is

Yeah, that's more like it.

So somehow RC goes from 1 to minus 1. Interesting :thinking:

BTW, instead of random for ease of debugging you may use:

let x: UInt32 = 0x11223344

It'd be quite satisfying to see that in debugger.

I take that back... that 0x200000.. is the address, perhaps the very first address allocated in the app.

Is it a big endian CPU or is it just getting displayed by int32 words? You will know the answer right away once you change your x to a known pattern.

I guess release is the next subject of investigation

some randomness to prevent optimisations

class CDontMangleMe {
        let x: UInt32

        init() {
            if Bool.random() {
                x = 0x1122_3344
            } else {
                x = 0x5566_7788
            }
        }
    }

As you've mentioned, in memory dump refcount goes from 0x01 to 0xffffffff

Edit: Nicer memory dump

So you are at the very beginning of release and RC is -1 already?! That's odd, should never be like that. Typically it could only be >=1 at that point.

It looks like someone is making the object immortal or some other dodgy play with RC.


Should there be a call to <free> before setting refcount to -1 ?

Plausible. We just don't see the full call stack, where this outlined_function_1 is getting called from and what is it about.

I went down the the call chain and

$ss13swift_release6objectyBp_tF
          $ss13swift_release6objectyBp_tF:
08000394:   push    {r4, r7, lr}
08000396:   add     r7, sp, #4
08000398:   str.w   r10, [sp, #-4]!
0800039c:   cbz     r0, 0x80003c8 <$ss13swift_release6objectyBp_tF+52>
0800039e:   ldr     r1, [r0, #4]
080003a0:   adds    r1, #1
080003a2:   beq.n   0x80003c8 <$ss13swift_release6objectyBp_tF+52>
080003a4:   adds    r2, r0, #4
080003a6:   dmb     sy
080003aa:   ldrex   r3, [r2]
080003ae:   subs    r1, r3, #1
080003b0:   strex   r4, r1, [r2]
080003b4:   cmp     r4, #0
080003b6:   bne.n   0x80003aa <$ss13swift_release6objectyBp_tF+22>
080003b8:   cmp     r1, r3
080003ba:   dmb     sy
080003be:   bvs.n   0x80003ce <$ss13swift_release6objectyBp_tF+58>
080003c0:   lsls    r1, r1, #1
080003c2:   bne.n   0x80003c8 <$ss13swift_release6objectyBp_tF+52>
080003c4:   bl      0x8000540 <OUTLINED_FUNCTION_1>
080003c8:   ldr.w   r10, [sp], #4
080003cc:   pop     {r4, r7, pc}
080003ce:   udf     #254    @ 0xfe

calls into

OUTLINED_FUNCTION_1
          OUTLINED_FUNCTION_1:
08000540:   mov.w   r1, #4294967295 @ 0xffffffff
08000544:   mov     r10, r0
08000546:   str     r1, [r0, #4]
08000548:   ldr     r1, [r0, #0]
0800054a:   ldr     r1, [r1, #4]
0800054c:   bx      r1

which loads an address of and calls into this one

$s4game16startSwiftEngineyyF13CDontMangleMeL_CfD
          $s4game16startSwiftEngineyyF13CDontMangleMeL_CfD:
08000241:   mov     r0, r10
08000243:   b.w     0x80002d4 <$ss26swift_deallocClassInstance6object13allocatedSize0F9AlignMaskyBp_S2itF>

and this

          $ss26swift_deallocClassInstance6object13allocatedSize0F9AlignMaskyBp_S2itF:
080002d5:   ldr     r1, [r0, #4]
080002d7:   cmp     r1, #0
080002d9:   bmi.n   0x80002de <$ss26swift_deallocClassInstance6object13allocatedSize0F9AlignMaskyBp_S2itF+10>
080002db:   b.w     0x80016dc <free>
080002df:   bx      lr

The last one compares refcount with 0 and returns without calling <free>
Looks like execution flow sets the RC to -1 and only then calls the actual deallocation routine $ss26swift_deallocClassInstance6object13allocatedSize0F9AlignMaskyBp_S2itF which in turn does nothing because it compares refcount (-1) with (0)

So OUTLINED_FUNCTION_1 makes object immortal for some reason, I guess there's nothing I can do in my code to fix it right ? :face_with_diagonal_mouth:

1 Like

Looks like a bug indeed.

Short term – you could probably patch that one instruction:

08000540:   mov.w   r1, #4294967295 @ 0xffffffff

to write 0 instead.

Btw the source code for this logic is here: swift/stdlib/public/core/EmbeddedRuntime.swift at main · swiftlang/swift · GitHub -- no need to read tea leaves from assembly.

3 Likes

Also might be useful to switch away from -Osize to -O or -Onone for a more debuggable program.

Thanks a lot !
Haha, we could have read the source :rofl:
Anyway, It was a lot of fun digging through it :nerd_face:

Since I'm on single core CPU can I patch it somehow ?
I'm thinking of just removing line 360

storeRelaxed(refcount, newValue: HeapObject.immortalRefCount)

Thank you! Although to read that source is not an easy task :slight_smile:

    // Set the refcount to immortalRefCount before calling the object destroyer
    // to prevent future retains/releases from having any effect. ...

    storeRelaxed(refcount, newValue: HeapObject.immortalRefCount)

    _swift_embedded_invoke_heap_object_destroy(object)

ok, that's what we saw in the asm. "make the object immortal just before we are about to kill it." Let's try to follow what "_swift_embedded_invoke_heap_object_destroy" is doing:

static inline void
_swift_embedded_invoke_heap_object_destroy(void *object) {
  void *metadata = ((EmbeddedHeapObject *)object)->metadata;
  void **destroy_location = &((void **)metadata)[1];
#if __has_feature(ptrauth_calls)
  (*(HeapObjectDestroyer __ptrauth(0, 1, 0xbbbf) *)destroy_location)(object);
#else
  (*(HeapObjectDestroyer *)destroy_location)(object);
#endif
}

ok, let's follow that destroy_location

  void **destroy_location = &((void **)metadata)[1];

Hmm. We now need to find where something was written to the metadata...

Quite easy to get lost here.

A little update! I've removed that make immortal line from swift runtime and build my own local Swift toolchain. Seems to work just fine without it, I'll post here once I ran into some issue caused by my patch :sweat_smile:

So far this is what I have


They bounce around and off of each other and it takes about 30~31 ms to render one frame, and most importantly program does not run out of memory in 5-ish sec.

3 Likes

Very cool! Interesting investigation and hopefully this can get patched for the future :)

Can you post the actual diff? Sounds like there's some real problem in the runtime that needs fixing upstream.

Here's the diff for root repo

diff1
> swift % git diff 88ac67d3773d5b64a088c41286e3718ec45d6f16
diff --git a/stdlib/public/core/EmbeddedRuntime.swift b/stdlib/public/core/EmbeddedRuntime.swift
index a737b16a850..eae8ca5d9b4 100644
--- a/stdlib/public/core/EmbeddedRuntime.swift
+++ b/stdlib/public/core/EmbeddedRuntime.swift
@@ -184,7 +184,7 @@ func isValidPointerForNativeRetain(object: Builtin.RawPointer) -> Bool {
   #if _pointerBitWidth(_64)
   if (objectBits & HeapObject.immortalObjectPointerBit) != 0 { return false }
   #endif
-
+
   return true
 }

@@ -309,7 +309,7 @@ func swift_release_n_(object: UnsafeMutablePointer<HeapObject>?, n: UInt32) {
     // There can only be one thread with a reference at this point (because
     // we're releasing the last existing reference), so a relaxed store is
     // enough.
-    storeRelaxed(refcount, newValue: HeapObject.immortalRefCount)
+    //storeRelaxed(refcount, newValue: HeapObject.immortalRefCount)

     _swift_embedded_invoke_heap_object_destroy(object)
   } else if resultingRefcount < 0 {
diff --git a/swift b/swift
new file mode 160000
index 00000000000..88ac67d3773
--- /dev/null
+++ b/swift
@@ -0,0 +1 @@
+Subproject commit 88ac67d3773d5b64a088c41286e3718ec45d6f16-dirty

and for swift

diff2
> swift % git diff 88ac67d3773d5b64a088c41286e3718ec45d6f16
diff --git a/stdlib/public/core/EmbeddedRuntime.swift b/stdlib/public/core/EmbeddedRuntime.swift
index a737b16a850..b646eb5b10d 100644
--- a/stdlib/public/core/EmbeddedRuntime.swift
+++ b/stdlib/public/core/EmbeddedRuntime.swift
@@ -184,7 +184,7 @@ func isValidPointerForNativeRetain(object: Builtin.RawPointer) -> Bool {
   #if _pointerBitWidth(_64)
   if (objectBits & HeapObject.immortalObjectPointerBit) != 0 { return false }
   #endif
-
+
   return true
 }

@@ -309,7 +309,7 @@ func swift_release_n_(object: UnsafeMutablePointer<HeapObject>?, n: UInt32) {
     // There can only be one thread with a reference at this point (because
     // we're releasing the last existing reference), so a relaxed store is
     // enough.
-    storeRelaxed(refcount, newValue: HeapObject.immortalRefCount)
+    // storeRelaxed(refcount, newValue: HeapObject.immortalRefCount)

     _swift_embedded_invoke_heap_object_destroy(object)
   } else if resultingRefcount < 0 {

In short I commented out

storeRelaxed(refcount, newValue: HeapObject.immortalRefCount)

in stdlib/public/core/EmbeddedRuntime.swift and in swift/stdlib/public/core/EmbeddedRuntime.swift

Not sure why I had two copies, I just followed the guide from here.