I've written code to incrementally decrypt a file using CommonCrypto. It allocates a 64-byte aligned buffer to put the decrypted data into, and then loads it 16 MiB at a time and decrypts it into that buffer. The resulting data is correct. What I can't figure out is why, in Xcode, it appears to leak. The file I’m loading is about 380 MiB. When I first allocate the destination buffer, I can see RAM use in Xcode jump by that much. Then in each pass through the loop, it jumps by another 16 MiB (give or take). I can't tell what in this code is holding onto memory.
This is the main loop. When it reads the next chunk, memory use goes up by 16 MiB (as expected). When it calls cryptorUpdate()
, memory goes up by another 16 MiB (as expected). When it loops back around to the top, memory goes down by 16 MiB. I would have expected it to go down by 32 MiB.
public
func
decrypt(input inInputURL: URL)
throws
-> Data
{
if !inInputURL.isFileURL
{
throw Error.inputNotFile
}
// Get the input file length. We have to seek
// to the end of the file then get the offset,
// and things changed in iOS 13.0…
let fh = try FileHandle(forReadingFrom: inInputURL)
var inputDataSize: UInt64 = 0
if #available(iOS 13.0, *)
{
try fh.__seek(toEndReturningOffset: &inputDataSize)
try fh.seek(toOffset: 0)
}
else
{
fh.seekToEndOfFile()
inputDataSize = fh.offsetInFile
fh.seek(toFileOffset: 0)
}
if inputDataSize > Int.max
{
throw Error.inputTooLarge
}
let cryptor = try cryptorCreate(op: CCOperation(kCCDecrypt), key: self.key, iv: self.iv)
// Allocate a 64 byte-aligned buffer for the output…
let startPtr = UnsafeMutableRawPointer.allocate(byteCount: Int(inputDataSize) + kCCBlockSizeAES128, alignment: 64)
startPtr.initializeMemory(as: UInt8.self, repeating: UInt8(0), count: Int(inputDataSize) + kCCBlockSizeAES128)
// Loop over the input file, reading data in small chunks…
var destPtr = startPtr
var outputCount = 0
var crypting = true
while crypting
{
// Read the next chunk of data…
let data = fh.readData(ofLength: kChunkSize)
// If the data read is zero bytes long, we’ve hit
// the end of file (having previously crypted the
// last of the data), so pad the resulint crypt…
let clearData: Data
if data.count == 0
{
clearData = try cryptorFinal(cryptor)
crypting = false
}
else
{
clearData = try cryptorUpdate(cryptor, input: data)
}
// Append it to the UnsafeMutableRawPointer…
clearData.withUnsafeBytes
{ clearBytes in
destPtr.copyMemory(from: clearBytes, byteCount: clearData.count)
}
destPtr = destPtr.advanced(by: clearData.count)
outputCount += clearData.count
}
CCCryptorRelease(cryptor)
assert(outputCount <= Int(inputDataSize) + kCCBlockSizeAES128)
let output = Data(bytesNoCopy: startPtr,
count: outputCount,
deallocator: .custom(
{ inPtr, inSize in
inPtr.deallocate()
}))
return output
}
this is the incremental decryption method:
private
func
cryptorUpdate(_ inCryptor: CCCryptorRef, input inData: Data)
throws
-> Data
{
let finalOutput = try inData.withUnsafeBytes
{ (inputBytes: UnsafePointer<UInt8>) -> Data in
var moved: Int = 0
let outputLength = CCCryptorGetOutputLength(inCryptor, inData.count, false);
var output = Data(count: outputLength)
try output.withUnsafeMutableBytes
{ [ count = output.count ] (outputBytes: UnsafeMutablePointer<UInt8>) -> () in
let status = CCCryptorUpdate(inCryptor, inputBytes, inData.count, outputBytes, count, &moved)
if status != kCCSuccess
{
throw Error.cryptoFailed(status: status)
}
assert(moved == outputLength)
}
return output
}
return finalOutput
}
Am I doing something wrong in here that's somehow hanging onto that memory? It's acting like I have Zombie Objects enabled, but I don't (and I don't even know if that applies to Swift).