Uploaded image for project: 'Swift'
  1. Swift
  2. SR-5810

Mutating a Data slice mutates it in a "nuclear fallout" sort of way

    XMLWordPrintable

    Details

    • Type: Bug
    • Status: Resolved
    • Priority: Medium
    • Resolution: Done
    • Component/s: Foundation
    • Labels:
      None
    • Environment:

      Swift version 4.1-dev (LLVM 4b4874ae4e, Clang c57d5b1d41, Swift 4264174392)
      Target: x86_64-apple-darwin16.7.0

      Description

      Suppose I have this function. It takes a Data object, makes a mutable copy, appends a byte to the beginning of the copy, and logs the result.

      func doSomething(with data: Data) {
          var mutableData = data
      	
          mutableData.insert(contentsOf: [0x01], at: mutableData.startIndex)
      
          print("modified data: \(mutableData as NSData))")
      }

      Seems reasonable enough, right? (Well, other than the expense of memmove'ing the whole data, but set that aside for a moment.) But. But! Every Data object can potentially be a slice. And if this Data happens to be a slice... well...

      let fullData = "Foo Bar Baz Qux".data(using: .utf8)!
      print("original: \(fullData as NSData)")
      
      var slice = fullData[(fullData.startIndex + 4)...]
      print("slice: \(slice as NSData)")
      
      doSomething(with: slice)

      Outputs:

      original: <466f6f20 42617220 42617a20 517578>
      slice: <42617220 42617a20 517578>
      modified data: <42617a20 01517578 00fa0700>)

      For some reason, the slice's startIndex seems to have migrated four bytes to the right, and the endIndex seems to go beyond the end of the buffer into random memory. And, that byte we tried to insert at the beginning got inserted somewhere in the middle instead. Huh.

      What happens if we delete something from the beginning of our Data, instead of add to it? Well:

      func doSomething(with data: Data) {
          var mutableData = data
      	
          if mutableData.count > 1 {
              mutableData.removeSubrange(mutableData.startIndex..<(mutableData.startIndex + 1))
          }
      
          print("modified data: \(mutableData as NSData))")
      }

      Output:

      original: <466f6f20 42617220 42617a20 517578>
      slice: <42617220 42617a20 517578>
      modified data: <42617a20 7578d6fb 0700>)

      Once again, the startIndex seems to have migrated four bytes to the right, we've got some random memory at the end, and instead of the first byte, we deleted something right in the middle. Okay.

      What happens if we add something to the end, you're wondering? Well, clearly the correct behavior for that case is to segfault.

      func doSomething(with data: Data) {
          var mutableData = data
      
          mutableData.append(0x01) // Terminated due to signal: SEGMENTATION FAULT (11)
      
          print("modified data: \(mutableData as NSData))")
      }

      Clearly, something is afoot with Data slices. You're shocked, I know. This should probably be fixed.

      ALTERNATIVES CONSIDERED:

      Just declare the result of using a data slice for any purpose as "undefined behavior."

      COMMENTARY:

      In all seriousness, it appears that the blame lies in the Copy-On-Write behavior for Data slices. If the startIndex is shifted from the beginning of the original Data (in this case, by four bytes), that shift gets re-applied to the slice, causing the shift to be twice as large as it should be. You could say... the COW gets tipped. I'll see myself out.

        Attachments

          Activity

            People

            Assignee:
            sashabelonogov Alexander Belonogov
            Reporter:
            CharlesJS Charles Srstka
            Votes:
            0 Vote for this issue
            Watchers:
            7 Start watching this issue

              Dates

              Created:
              Updated:
              Resolved: