swift-foundation icon indicating copy to clipboard operation
swift-foundation copied to clipboard

Calling Date.description might result in undefined behaviour

Open dhoepfl opened this issue 2 years ago • 3 comments

Date’s description function uses a fixed size buffer (26 bytes, see here). This buffer is given to strftime.

Both, dates before “-999-01-01 00:00:00 +0000” and starting at “10000-01-01 00:00:00 +0000”, will require 27 bytes or more (including null byte at the end).

The return value of this function is checked by guard strftime(…) >= 0 (see). While most library functions in C return a negative value on failure, strftime returns 0 if the buffer is not big enough to hold the formatted date. Thus this guard never executes.

The buffer in this case is not guaranteed to contain a ending null byte, as the man page states: “Otherwise, 0 shall be returned and the contents of the array are unspecified.” (Highlighting mine)

The undefined content (worst case: 26 bytes forming valid UTF-8, e.g. 26 spaces) is then given to String(validatingUTF8:) which probably will (in the worst case) run behind the end of the buffer, resulting in a crash.

I suggest to either replace the call to strftime by a better API or change the buffer size to be big enough to hold any description of any point in time that Date might represent. In addition to that, the guard should be changed to guard strftime(…) >= 25 given that is the smallest valid size.

test cases:

Date(timeIntervalSinceReferenceDate: TimeInterval(-63145526400)).description == "0000-01-01 00:00:00 +0000"
Date(timeIntervalSinceReferenceDate: TimeInterval(-4200000000000)).description == "-131090-12-30 21:20:00 +0000"
Date(timeIntervalSinceReferenceDate: TimeInterval(4200000000000)).description == "15310-04-10 02:40:00 +0000"

dhoepfl avatar Apr 28 '23 15:04 dhoepfl

I'd rather start with 26 bytes like we do now and allow for one single reallocation if strftime returns 0 - which is guaranteed (in our specific case / given the format) to be due to undersizing the buffer.

I slightly disagree with guard strftime(…) >= 25 (although true in practice), but it should not be required if we're looking for 0 directly (as per man).

valentinradu avatar Apr 28 '23 18:04 valentinradu

The memory is deallocated at the end of the block, why not use a few bytes more than the minimum? Thinking about that, I’d use something that just allocates the buffer on the stack:

   lazy var unavailable = { "<description unavailable>" } ()
   let format = "%Y-%m-%d %H:%M:%S +0000"
   let bufferSize = 512

   var info = tm()
   var time = time_t(self.timeIntervalSince1970)
   gmtime_r(&time, &info)

   return withUnsafeTemporaryAllocation(of: CChar.self, capacity: bufferSize) { buffer -> String in
      guard let ptr = buffer.baseAddress else {
         return unavailable
      }

      guard strftime(ptr, bufferSize, format, &info) != 0 else {
         return unavailable
      }

      guard let result = String(validatingUTF8: ptr) else {
         return unavailable
      }

      return result
   }

I know, 512 bytes is unreasonable.

On the other hand, currently the compiler considers up to 1024 bytes as small enough to live on the stack (see).

It could be whatever the is used at most (with the given max time_t) but I think, I’d just use 64 or 128 and it will work with every strange architecture even when time_t is way bigger.

dhoepfl avatar Apr 28 '23 20:04 dhoepfl

Looks solid 🚀. Out of curiosity, why not let unavailable instead (since we're dealing with a string literal anyway)?

My only argument against a large initial buffer was that in 99.999% of the cases the buffer is exactly 26 bytes. That being said, you're right, there's literally no gain to be had, so we might as well allocate 512.

valentinradu avatar Apr 29 '23 00:04 valentinradu

Looks solid 🚀. Out of curiosity, why not let unavailable instead (since we're dealing with a string literal anyway)?

I was over-optimizing. (I thought that assigning the string literal to the variable would require a copy of the string.)

dhoepfl avatar Apr 30 '23 18:04 dhoepfl

Fixed by https://github.com/apple/swift-foundation/pull/93

glessard avatar May 09 '23 21:05 glessard