Austin Group Defect Tracker

Aardvark Mark IV


Viewing Issue Simple Details Jump to Notes ] Issue History ] Print ]
ID Category Severity Type Date Submitted Last Update
0001627 [1003.1(2016/18)/Issue7+TC2] System Interfaces Objection Enhancement Request 2023-01-05 12:17 2023-09-04 10:34
Reporter kre View Status public  
Assigned To
Priority normal Resolution Accepted As Marked  
Status Applied  
Name Robert Elz
Organization
User Reference
Section XSH 3/mktime
Page Number 1331
Line Number 44310-44332, 44361
Interp Status ---
Final Accepted Text Note: 0006415
Summary 0001627: XSH 3 / mktime() is woefully underspecified
Description Following on from notes added to bug:1614 and a lengthy
mailing list discussion, it is evident that the specification
of XSH/mktime is woefully inadequate.

New text is specified in the Desired Action to remedy those defects.

This is currently missing anything dealing with what should be done
if the input tm_isdst is not < 0, and does not agree (in sign, if 0
can be said to have a sign) with the final value for tm_isdst in the
struct tm on a successful return.

That's because my inclination is to simply do nothing in that case,
return the correct tm_isdst, but otherwise ignore it - but I admit that's
not how the implementations behave, and that may be being depended upon
by some applications (though the current behaviour is definitely not
required by any standard). So I will leave it for someone who cares
about that to add suitable text to (properly) specify what is to happen.

Also, given that it is too late now to consider adding a timegm()
function (an analog to mktime() which has existed for decades, but
never been standardised) I thought what might be possible would be
to specify enough in the FUTURE DIRECTIONS here to indicate that that
will happen (since it is being added to the C standard, it will happen,
eventually) and to indicate why using it is a much better idea when
the purpose is time_t arithmetic than using localtime()/mktime().
The intent is to get applications to start writing safe code, rather
than nonsense, and do that asap - since in practice, timegm() is
already widely available.

As usual, formatting and wording changes, which keep to the general
intent expressed below are welcome. One thing I considered, but
wasn't sure of a good way to handle, was to find some shorter way
to express "the converted value of the struct tm referred to by
timeptr" (or a field thereof) - which occurs far too often in the
text below, and is unwieldy.
Desired Action Delete the (CX shaded) paragraph that starts (line 44310)

        A positive or 0 value for tm_isdst ...
and ends (line 44313)
        ... is in effect for the specified time.

Replace the (CX) shaded paragraph that starts (line 44315)

        The relationship between the tm structure ...
and ends(line 44321)
        ... the other tm structure members specified in <time.h>
        (excluding tm_wday).

with the following (presumably also CX shaded) paragraphs:

        The mktime() function will first convert the tm_sec, tm_min,
        tm_hour, tm_mon, tm_mday and tm_mon (again) fields of the tm
        structure referenced by timeptr (or a local internal copy thereof),
        in that order, so that their values become within the ranges specified
        by <time.h>, but also within the ranges applicable to a Gregorian
        Calendar date (tm_sec shall not be more than 59, and tm_mday shall
        not be more than the number of days in the month specified by
        the tm_mon field of the year specified by the tm_year field).

        If _field_ represents the field being converted, and _next_
        represents the field specified immediately after it in <time.h>
        then this shall be done, for each field, by an algorithm equivalent
        to:

                if (timeptr->_field_ < MINIMUM_VALUE) {
                        while (timeptr->_field_ < MINIMUM_VALUE) {
                                timeptr->_field_ += RANGE;
                                timeptr->_next_--; /* check overflow */
                        }
                } else if (timeptr->_field_ > MAXIMUM_VALUE) {
                        while (timeptr->_field_ > MAXIMUM_VALUE) {
                                timeptr->_field_ -= RANGE;
                                timeptr->_next_++; /* check overflow */
                        }
                } /* else do nothing, value of _field_ is OK */

        where MINIMUM_VALUE is the minimum allowed value for the
        field _field_ as specified in <time.h> MAXIMUM_VALUE is the
        maximum allowed value for the field _field_ as specified in
        <time.h> except that it shall be 59 where _field_ is tm_sec,
        and shall be the appropriate number of days in the specific
        month selected by the tm_mon and tm_year fields, where _field_
        is tm_mday, and thus is subject to change during each iteration
        of the loop, and RANGE is (MAXIMUM_VALUE - MINIMUM_VALUE + 1)
        (which is also subject to change, in each iteration of both loops
        above, where the field is tm_mday).

        Note that there is no requirement that the actual structure
        passed via *timeptr be the one being modified by this code.

        Should overflow (absolute value of the field becomes too large
        to be represented in an int) occur at the places indicated,
        the implementation shall return an error if the _next_ field is
        tm_year, and may return an error for other fields, though if
        _next_ is not tm_year, it may adjust the value of any later field,
        and reduce the magnitude of the _next_ field by an appropriate
        amount to compensate. Adjustments made this way should be chosen
        so as to minimise the effects of the adjustment upon the meaning
        of the later field, for example, if tm_hour were to overflow,
        the implementation might adjust tm_mday by 146101 (the number of
        days in a 400 year period - since in the Gregorian calendar, that is
        a constant) and reduce the magnitude of tm_hour by 3506424 (24*146101,
        the number of hours in 400 years). Alternatively it might alter
        tm_mon by 4800 (the number of months in a 400 year period), and
        adjust tm_hour by the same amount (3506424). Overflow produced
        when making any such adjustment should be handled in a similar
        way, including, if an adjustment to tm_mon requires an adjustment
        to tm_year, and that causes tm_year to overflow, then an error
        shall be returned.

        The tm_isdst field of the structure referred to by timeptr (or
        a local copy thereof) shall be converted by altering any
        value that is less than 0 to be -1, and any value that is
        greater than 0 to be 1. If supplied as 0, no change shall
        be made.

        Once all fields are within the appropriate ranges, the
        implementation shall determine if there is a unique value
        of the type returned by time() (which is expressed as a value
        in Coordinated Universal Time) which when converted to a
        struct tm by a function equivalent to localtime() would
        produce identical values for the tm_sec tm_min tm_hour tm_mday
        tm_mon and tm_year fields of the converted input struct tm.
        This may be accomplished by applying a formula, similar to
        that specified for Coordinated Universal Time in <xref XBD 4.17>
        adjusted to account for local timezone offsets, and time
        alterations, or by any other means.

        If such a unique result is found, then that shall be the
        result from mktime().

        If no result is found because the tm structure represents
        a value outside the range of values that can be represented
        by a value returned by time(), then an error shall be returned.

        Otherwise if no result is able to be found, then the local time
        specified represents a time which does not exist as a local time
        value. In this case, if the value of tm_isdst in the struct tm
        specified by timeptr is greater than or equal to 0, and there
        are two values or the type returned by time(), representing times
        that are one second apart, (t1 and t2, where t2 == t1 + 1 second)
        which can be found of the type returned by time(), such that
        one of those, when converted by a function equivalent to localtime()
        returns a time which occurs before the converted time referred to
        by timeptr, and the other returns a time which occurs later, and
        also one of those would produce a struct tm with tm_isdst == 0,
        and the other when converted by localtime would produce a struct tm
        with tm_isdst == 1, then if the application's converted tm_isdst
        field the same as that produced by t1, then the implementation
        shall calculate the difference, in seconds, between the converted
        time specified by timeptr, and that produced by a conversion of t1,
        add the number of seconds to t1, and that shall be the result of
        mktime. Otherwise, if the applications converted tm_isdst is
        the same as that produced by t2, the implementation shall
        calculate the difference (in seconds) between the struct tm
        produced by t2, and that specified by the converted struct tm
        referred to by timeptr, and subtract that number of seconds from
        t2, and that shall be the result from mktime(). In any other
        case the result is unspecified. The implementation may
        arbitrarily return one of the results as if it had been one of
        the two specified cases, or may return an error.

        If more than one possible result is found, then if there are
        exactly two possible results, and one of those, when converted by
        a function equivalent to localtime(), produces a value with tm_isdst
        having the same value as the converted value of that field in the
        struct tm referred to by timeptr, and the other does not, then
        the result of mktime() shall be the single unique result which
        produces a struct tm with the same tm_sec tm_min tm_hour tm_mday
        tm_mon tm_year and tm_isdst fields as the converted values in the
        struct tm referred to by timeptr. In any other case, the result
        is unspecified. The implementation may arbitrarily return any
        of the plausible ambiguous results, or may return an error.

This should then be followed by the new (bug 1613 inserted) text about
what happens to the struct tm in the case of a successful return. This
I believe has already replaced the "Upon successful completion, the values
of the tm_wday..." paragraph. If not, delete whatever is left of it.

A new paragraph (or just sentence perhaps) should be added after the 1613
inserted paragraph:

        When mktime() returns an error, the contents of the structure
        referred to by timeptr, after mktime() returns, shall be unspecified.

The RETURN VALUE section (lines 44327-9) should be replaced by:

        The mktime() function shall return the calculated time since the
        epoch, as specified in the DESCRIPTION, encoded as a value of
        type time_t. If an error is to be returned, then the function
        shall return the value (time_t)-1, and set errno to indicate the
        error.

The ERRORS section (lines 44331-2) should be replaced by

        The mktime() function shall fail if:

        [EOVERFLOW] The value of the time returned by time() which
                        represents the converted struct tm passed by
                        timeptr falls outside the range of values that
                        can be represented as a time_t.

        [EOVERFLOW] While correcting the values of the fields of the
                        struct tm referred to by timeptr to be within the
                        required ranges, a required adjustment of the tm_year
                        field caused that field to overflow.

        The mktime() function may fail if:

        [EOVERFLOW] Adjusting a field of the struct tm referred to
                        by timeptr caused an adjustment to be required to
                        another field, and that adjustment caused that other
                        field to overflow.

        [EINVAL] The converted struct tm referred to by timeptr
                        cannot be represented by a unique number of seconds
                        past the epoch, Coordinated Universal Time, and
                        the input values, and/or circumstances are not such
                        that an alternative is required to be selected.

In the FUTURE DIRECTIONS section (line 44361) replace "None." by

        A later edition of the standard is expected to add a timegm()
        function that is similar to mktime(), except that the struct tm
        referred to by timeptr represents a calendar time in Coordinated
        Universal Time (rather than the local time zone), where references
        to localtime() are replaced by references to gmtime(), and where
        there are no zone offset adjustments, or missing or ambiguous times,
        tm_isdst is always 0, and EINVAL cannot be returned. A combination
        of gmtime() and timegm() will be the expected way to perform
        arithmetic upon a time_t value and remain compatible with the C
        standard (where the internal structure of a time_t is not specified).
        Attempting such manipulations using localtime() and mktime() can lead
        to unexpected results.
Tags applied_after_i8d3, issue8
Attached Files

- Relationships
related to 0001614Applied XSH 3/mktime does not specify EINVAL and should 

-  Notes
(0006118)
mirabilos (reporter)
2023-01-20 20:54

Don’t break mktime of a valid struct with tm_sec=60 pointing to a leap second, please.
(0006415)
geoffclare (manager)
2023-08-07 09:05
edited on: 2023-08-07 09:06

Suggested resolution ...

On page 113 line 3186 section 4.16 Seconds Since the Epoch, change:
The relationship between the actual time of day and the current value for seconds since the Epoch is unspecified.
to:
The relationship between the actual date and time in Coordinated Universal Time, as determined by the International Earth Rotation Service, and the system's current value for seconds since the Epoch is unspecified.

On page 1331 line 44320 section mktime(), after applying bugs 1613 and 1614 change:
The relationship between the tm structure (defined in the <time.h> header) and the time in seconds since the Epoch is that the result shall be as specified in the expression given in the definition of seconds since the Epoch (see [xref to XBD 4.19]) corrected for the offset of the timezone's standard time from Coordinated Universal Time and further corrected (if applicable--see below) for Daylight Saving Time, where the names other than tm_yday in the structure and in the expression correspond, and the tm_yday value used in the expression is the day of the year from 0 to 365 inclusive, calculated from the members of the tm structure specified above.
to:
The mktime() function shall calculate the time in seconds since the Epoch to be returned as if by manipulating the members of the tm structure according to the following steps.

1. The tm_sec member may, but should not, be brought into the range 0 to 60, inclusive. For each 60 seconds added to or subtracted from tm_sec, a decrement or increment, respectively, of 1 minute shall be saved for later application.

2. The tm_min member shall be brought into the range 0 to 59, inclusive, and any saved decrement or increment of minutes shall then be applied, repeating the range adjustment afterwards if necessary. For each 60 minutes added to or subtracted from tm_min, a decrement or increment, respectively, of 1 hour shall be saved for later application.

3. The tm_hour member shall be brought into the range 0 to 23, inclusive, and any saved decrement or increment of hours shall then be applied, repeating the range adjustment afterwards if necessary. For each 24 hours added to or subtracted from tm_hour, a decrement or increment, respectively, of 1 day shall be saved for later application.

4. The tm_mon member shall be brought into the range 0 to 11, inclusive. For each 12 months added to or subtracted from tm_mon, a decrement or increment, respectively, of 1 year shall be saved for later use.

5. The tm_mday member shall be brought into the range 1 to 31, inclusive, and any saved decrement or increment of days shall then be applied, repeating the range adjustment afterwards if necessary. Adjustments downwards shall be applied by subtracting the number of days (according to the Gregorian calendar) in month tm_mon+1 of the year obtained by adding/subtracting any saved increment/decrement of years to the value tm_year+1900, and then incrementing tm_mon by 1, repeated as necessary. Adjustments upwards shall be applied by adding the number of days in the month before month tm_mon+1 of the year obtained by adding/subtracting any saved increment/decrement of years to the value tm_year+1900, and then decrementing tm_mon by 1, repeated as necessary. During these adjustments, the tm_mon value shall be kept within the range 0 to 11, inclusive, by applying step 4 as necessary.

6. If the tm_mday member is greater than the number of days in month tm_mon+1 of the year obtained by adding/subtracting any saved increment/decrement of years to the value tm_year+1900, that number of days shall be subtracted from tm_mday, and tm_mon shall be incremented by 1. If this results in tm_mon having the value 12, step 4 shall be applied.

7. The number of seconds since the Epoch in Coordinated Universal Time shall be calculated from the range-corrected values of the relevant tm structure members (or the original value where a member was not range corrected) as specified in the expression given in the definition of seconds since the Epoch (see [xref to XBD 4.19]), where the names other than tm_year and tm_yday in the structure and in the expression correspond, the tm_year value used in the expression is the tm_year in the structure plus/minus any saved increment/decrement of years, and the tm_yday value used in the expression is the day of the year from 0 to 365 inclusive, calculated from the tm_mon and tm_mday members of the tm structure, for that year.

8. The time since the Epoch shall be corrected for the offset of the local timezone's standard time from Coordinated Universal Time.

9. The time since the Epoch shall be further corrected (if applicable--see below) for Daylight Saving Time.

On page 1332 line 44357 section mktime(), change APPLICATION USAGE from "None" to:
When using mktime() to add or subtract a fixed time period (one that always corresponds to a fixed number of seconds) to or from a broken-down time in the local timezone, reliable results for arbitrary TZ can only be assured by using mktime() to convert the original broken-down time to a time since the Epoch, adding or subtracting the desired number of seconds to that value, and then calling localtime() with the result. The alternative of adjusting the broken-down time before calling mktime() may produce unexpected results if the original and updated times are on different sides of a geographical timezone change. On implementations that follow the recommendation of not range-correcting tm_sec (see step 1 in the DESCRIPTION), reliable results can also be assured by adding or subtracting the desired number of seconds to tm_sec (and not modifying any other members of the tm structure). In applications needing to be portable to non-POSIX systems where the time_t encoding is not a count of seconds, it is recommended that conditional compilation is used such that the adjustment is performed on the mktime() return value when possible, and otherwise on the tm_sec member. For timezones that are known not to have geographical timezone changes, such as <tt>TZ=UTC0</tt>, adjustments using just mktime() do not have this problem.

The way the mktime() function interprets out-of-range tm structure fields might not produce the expected result when multiple adjustments are made at the same time. For example, if an application tries to go back one day first and then one year by calling localtime(), decrementing tm_mday and tm_year, and then calling mktime() this would not produce the expected result if it was called on 2021-03-01 because mktime() would see the supplied year as 2020 (a leap year) and correct Mar 0 to Feb 29, whereas the intended result was Feb 28. Such issues can be avoided by doing multiple adjustments one at a time when the order in which they are done matters.

Examples of how mktime() handles some adjustments are:
  • If given Feb 29 in a non-leap year it treats that as the day after Feb 28 and gives back Mar 1.

  • If given Feb 0 it treats that as the day before Feb 1 and gives back Jan 31.

  • If given 21:65 it treats that as 6 minutes after 21:59 and gives back 22:05.

  • If given tm_isdst=0 for a time when DST is in effect, it gives back a positive tm_isdst and alters the other fields appropriately.

  • If there is a DST transition where 02:00 standard time becomes 03:00 DST and mktime() is given 02:30 (with negative tm_isdst), it treats that as either 30 minutes after 02:00 standard time or 30 minutes before 03:00 DST and gives back a zero or positive tm_isdst, respectively, with the tm_hour field altered appropriately.

  • If a geographical timezone changes its UTC offset such that ``old 00:00'' becomes ``new 00:30'' and mktime() is given 00:20, it treats that as either 20 minutes after ``old 00:00'' or 10 minutes before ``new 00:30'', and gives back appropriately altered struct tm fields.

If an application wants to check whether a given broken-down time is one that is skipped over, it can do so by seeing whether the tm_mday, tm_hour, and tm_min values it gets back from mktime() are the same ones it fed in. Just checking tm_hour and tm_min might appear at first sight to suffice, but tm_mday could also change--without tm_hour and tm_min changing--if, for example, TZ is set to "ABC12XYZ-12" (which might be used in a torture test) or if a geographical timezone changes the offset from Coordinated Universal Time of its standard time by 24 hours.

On page 1332 line 44359 section mktime(), change RATIONALE from "None" to:
Implementations are encouraged not to range-correct tm_sec (see step 1 in the DESCRIPTION) in order for the results of making an adjustment to tm_sec always to be equivalent to making the same adjustment to the value returned by mktime(), even when the original and updated times are on different sides of a geographical timezone change. This provides a way for applications to do reliable fixed-period adjustment using only mktime(), as described in APPLICATION USAGE.

The described method for range-correcting the tm structure members uses separate variables to hold adjustment values to be applied later to other members, or (for the year adjustment) used in later calculations, because this is one way of avoiding intermediate member values that are not representable as an int. Implementations may use other methods; all that is required is that tm_year is the only member for which an [EOVERFLOW] error can occur.

The described method for range-correcting tm_mday would, if implemented that way, be highly inefficient for very large values. The efficiency can be improved by observing that any period of 400 years always has the same number of days, so the month-by-month correction method need only be applied for a maximum of 4800 months.

On page 1332 line 44361 section mktime(), change FUTURE DIRECTIONS from "None" to:
A future version of this standard may require that mktime() does not perform the optional range correction of the tm_sec member of the tm structure described at step 1 in the DESCRIPTION.

A future version of this standard is expected to add a timegm() function that is similar to mktime(), except that the tm structure pointed to by timeptr contains a broken-down time in Coordinated Universal Time (rather than the local timezone), where references to localtime() are replaced by references to gmtime(), and where there are no timezone offset or Daylight Saving Time adjustments. A combination of gmtime() and timegm() will be the expected way to perform arithmetic upon a time_t value and remain compatible with the ISO C standard (where the internal structure of a time_t is not specified), since attempting such manipulations using localtime() and mktime() can lead to unexpected results.


(0006429)
mirabilos (reporter)
2023-08-14 12:01

Much love for the extra effort here, and to distinguish between “go forward one minute” and “go forward 60 seconds” is interesting in “breaks in time” cases (like leap seconds). Thank you!

- Issue History
Date Modified Username Field Change
2023-01-05 12:17 kre New Issue
2023-01-05 12:17 kre Name => Robert Elz
2023-01-05 12:17 kre Section => XSH 3/mktime
2023-01-05 12:17 kre Page Number => 1331
2023-01-05 12:17 kre Line Number => 44310-44332, 44361
2023-01-20 20:54 mirabilos Note Added: 0006118
2023-07-25 14:09 geoffclare Relationship added related to 0001614
2023-08-07 09:05 geoffclare Note Added: 0006415
2023-08-07 09:06 geoffclare Note Edited: 0006415
2023-08-14 12:01 mirabilos Note Added: 0006429
2023-08-14 15:16 geoffclare Interp Status => ---
2023-08-14 15:16 geoffclare Final Accepted Text => Note: 0006415
2023-08-14 15:16 geoffclare Status New => Resolved
2023-08-14 15:16 geoffclare Resolution Open => Accepted As Marked
2023-08-14 15:16 geoffclare Desired Action Updated
2023-08-14 15:17 geoffclare Tag Attached: issue8
2023-09-04 10:34 geoffclare Status Resolved => Applied
2023-09-04 10:34 geoffclare Tag Attached: applied_after_i8d3


Mantis 1.1.6[^]
Copyright © 2000 - 2008 Mantis Group
Powered by Mantis Bugtracker