Swift optimizer considered harmful

For some time I struggled with one weird bug reported to CryptoSwift. Few people report that sometimes they got mangled result of decryption with AES. Turn out the code that produce mangled output was PKCS7 class, with part responsible for removing padded bytes.

this very function:

public func remove(bytes: [UInt8], blockSize:Int? = nil) -> [UInt8] {
    var padding = Int(bytes.last!)
    
    if padding >= 1 {
        return Array(bytes[0..<(bytes.count - padding)])
    }
    return bytes
}

when I look at the code, everything is ok.

The code is right

The function is about removing as many bytes as denoted by the last byte in the array. The code is right. I did some tests, all test passed.

For given input array of bytes:

var arr:[UInt8] = [84, 104, 105, 115, 32, 105, 115, 32, 109, 121, 32, 111, 114, 105, 103, 105, 110, 97, 108, 32, 116, 101, 120, 116, 8, 8, 8, 8, 8, 8, 8, 8]
  1. var padding = Int(bytes.last!) result 8
  2. return Array(bytes[0..<(bytes.count - padding)]) result with array, without last 8 bytes: [84, 104, 105, 115, 32, 105, 115, 32, 109, 121, 32, 111, 114, 105, 103, 105, 110, 97, 108, 32, 116, 101, 120, 116]

the code is right!

The result is wrong!

Then @gyamana pointed out in the comments that the issue is reproducible only on device. I can't simply run tests on device, because frameworks unit tests can't be run on device (huh!). So I verified the result of the function, the very same function. This is the result:

  1. var padding = Int(bytes.last!) result 1, should be 8.
  2. return Array(bytes[0..<(bytes.count - padding)]) result with array, without last byte: [84, 104, 105, 115, 32, 105, 115, 32, 109, 121, 32, 111, 114, 105, 103, 105, 110, 97, 108, 32, 116, 101, 120, 116, 8, 8, 8, 8, 8, 8, 8]

as you can see: final result is wrong.

Let me capitalise the error:

Int(bytes.last!) returns with different value: "8" on Simulator vs "1" on actual Device.

Workaround

The workaround is to split conversion to Int in two lines

  1. save result of unwrapping to the variable
  2. use the variable to initiate new Int
    let last = bytes.last!  
    var padding = Int(last)  // ok now
Testing

I did some more tests to separate the issue. What I observe is that value of "padding" is not right and It is reproducible with newly created project.

My function is a method of the class Test

class Test {
    func remove(bytes: [UInt8]) -> Int {
        var padding = Int(bytes.last!)
        
        if padding >= 1 {
            return padding
        }
        return padding
    }
}

and test

        let arr:[UInt8] = [84, 104, 105, 115, 32, 105, 115, 32, 109, 121, 32, 111, 114, 105, 103, 105, 110, 97, 108, 32, 116, 101, 120, 116, 8, 8, 8, 8, 8, 8, 8]
        let test = Test()
        print(test.remove(arr))

result:

  • on device: 1 - wrong
  • on simulator: 8 - right

Optimization

Finally I checked the optimization flags. The problem is reproducible with code generated without optimization (-Onone) for ARM (because for x86 it is OK). At the same time, result is right with optimized code (-O, -Ounchecked) - this is rather strange to me, I'd expect unoptimized code free of errors like this, rather that optimized one.

This is what I call: write once, debug many.

Conclusion

My code may produce buggy output depends on architecture - I'd understand this for C, this is a C problem, but Swift is not C, it is much higher level. I don't like surprises like this one.

One thing bugs me. Wonder if Bitcode can fix issues like this automatically. It seems the tool to do the job.

Bitcode. When you archive for submission to the App Store, Xcode will compile your app into an intermediate representation. The App Store will then compile the bitcode down into the 64 or 32 bit executables as necessary.

Don't trust your code

Don't trust your code, optimizations might broke it anyway. Don't trust Simulator results and test everything on device. Test frameworks, test it all with all available optimalization flags. That said.... easy to say, harder to make it happen this way.

It's just a bug, after all. This case undermine my confidence to the correctness of Swift.

Xcode 6.3.2, Swift 1.2, iPhone 6

Test case code is available at gist: https://gist.github.com/krzyzanowskim/7aaf6a46e3c4f2270b78

PS. This case is not reproducible with Xcode 7 beta1.

rdar://21476680

Cover photo: https://www.flickr.com/photos/36107339@N03/9407392059