[PROPOSED 1/7] Avoid undefined behavior if no Link lines
* zic.c (make_links): Don't call qsort(NULL, 0, ...) as this has undefined behavior in C. This fixes a bug introduced five days ago. --- zic.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zic.c b/zic.c index 78afa67..a699285 100644 --- a/zic.c +++ b/zic.c @@ -683,7 +683,8 @@ static void make_links(void) { ptrdiff_t i, j, nalinks; - qsort(links, nlinks, sizeof *links, qsort_linkcmp); + if (nlinks) + qsort(links, nlinks, sizeof *links, qsort_linkcmp); /* Ignore each link superseded by a later link with the same name. */ j = 0; -- 2.37.3
When converting to and from vanguard form, line up the columns in ‘backward’ more nicely. This does not change the data’s meaning; it merely inserts and deletes tabs. * backward: Revert yesterday’s formatting changes, restoring the file’s more nicely-indented columns. * ziguard.awk (make_linkline): New function, which lines up columns. (/^Link/, cut_link_chains_short): Use it. --- backward | 10 +++++----- ziguard.awk | 49 ++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/backward b/backward index 46dc17d..4c1c5d5 100644 --- a/backward +++ b/backward @@ -89,7 +89,7 @@ Link Etc/GMT GMT0 Link Etc/GMT Greenwich # End of rearguard section. Link Asia/Hong_Kong Hongkong -Link Africa/Abidjan Iceland #= Atlantic/Reykjavik +Link Africa/Abidjan Iceland #= Atlantic/Reykjavik Link Asia/Tehran Iran Link Asia/Jerusalem Israel Link America/Jamaica Jamaica @@ -101,7 +101,7 @@ Link America/Mazatlan Mexico/BajaSur Link America/Mexico_City Mexico/General Link Pacific/Auckland NZ Link Pacific/Chatham NZ-CHAT -Link America/Denver Navajo #= America/Shiprock +Link America/Denver Navajo #= America/Shiprock Link Asia/Shanghai PRC Link Europe/Warsaw Poland Link Europe/Lisbon Portugal @@ -139,7 +139,7 @@ Link America/Argentina/Jujuy America/Jujuy Link America/Indiana/Knox America/Knox_IN Link America/Kentucky/Louisville America/Louisville Link America/Argentina/Mendoza America/Mendoza -Link America/Puerto_Rico America/Virgin #= America/St_Thomas +Link America/Puerto_Rico America/Virgin #= America/St_Thomas Link Pacific/Pago_Pago Pacific/Samoa @@ -293,11 +293,11 @@ Link Pacific/Port_Moresby Pacific/Yap # Alternate names for the same location # Link TARGET LINK-NAME #= TARGET1 -Link Africa/Nairobi Africa/Asmera #= Africa/Asmara +Link Africa/Nairobi Africa/Asmera #= Africa/Asmara Link America/Nuuk America/Godthab Link Asia/Ashgabat Asia/Ashkhabad Link Asia/Kolkata Asia/Calcutta -Link Asia/Shanghai Asia/Chungking #= Asia/Chongqing +Link Asia/Shanghai Asia/Chungking #= Asia/Chongqing Link Asia/Dhaka Asia/Dacca # Istanbul is in both continents. Link Europe/Istanbul Asia/Istanbul diff --git a/ziguard.awk b/ziguard.awk index 8cff035..7a3404f 100644 --- a/ziguard.awk +++ b/ziguard.awk @@ -311,8 +311,37 @@ DATAFORM != "main" { } } +# Return a link line resulting by changing OLDLINE to link to TARGET +# from LINKNAME, instead of linking to OLDTARGET from LINKNAME. +# Align data columns the same as they were in OLDLINE. +# Also, replace any existing white space followed by comment with COMMENT. +function make_linkline(oldline, target, linkname, oldtarget, comment, \ + oldprefix, oldprefixlen, oldtargettabs, \ + replsuffix, targettabs) +{ + oldprefix = "Link\t" oldtarget "\t" + oldprefixlen = length(oldprefix) + if (substr(oldline, 1, oldprefixlen) == oldprefix) { + # Use tab stops to preserve LINKNAME's column. + replsuffix = substr(oldline, oldprefixlen + 1) + sub(/[\t ]*#.*/, "", replsuffix) + oldtargettabs = int(length(oldtarget) / 8) + 1 + targettabs = int(length(target) / 8) + 1 + for (; targettabs < oldtargettabs; targettabs++) { + replsuffix = "\t" replsuffix + } + for (; oldtargettabs < targettabs && replsuffix ~ /^\t/; targettabs--) { + replsuffix = substr(replsuffix, 2) + } + } else { + # Odd format line; don't bother lining up its replacement nicely. + replsuffix = linkname + } + return "Link\t" target "\t" replsuffix comment +} + /^Link/ && $4 == "#=" && DATAFORM == "vanguard" { - $0 = "Link\t" $5 "\t" $3 + $0 = make_linkline($0, $5, $3, $2) } # If a Link line is followed by a Link or Zone line for the same data, comment @@ -329,15 +358,21 @@ DATAFORM != "main" { { line[NR] = $0 } -function cut_link_chains_short(\ - linkname, target, t, u) +function cut_link_chains_short( \ + l, linkname, t, target) { for (linkname in linktarget) { target = linktarget[linkname] - for (t = target; (u = linktarget[t]); t = u) - continue; - if (t != target) { - line[linkline[linkname]] = "Link\t" t "\t" linkname "\t#= " target + t = linktarget[target] + if (t) { + # TARGET is itself a link name. Replace the line "Link TARGET LINKNAME" + # with "Link T LINKNAME #= TARGET", where T is at the end of the chain + # of links that LINKNAME points to. + while ((u = linktarget[t])) { + t = u + } + l = linkline[linkname] + line[l] = make_linkline(line[l], t, linkname, target, "\t#= " target) } } } -- 2.37.3
* zdump.c (HAVE_GENERIC): Remove definition that is unused because private.h already defines it. I put this definition in by mistake in my 2022-01-25 patch. --- zdump.c | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/zdump.c b/zdump.c index ef6cb8b..168f72a 100644 --- a/zdump.c +++ b/zdump.c @@ -56,21 +56,6 @@ */ enum { SECSPER400YEARS_FITS = SECSPERLYEAR <= INTMAX_MAX / 400 }; -#if !defined HAVE_GENERIC && defined __has_extension -# if __has_extension(c_generic_selections) -# define HAVE_GENERIC 1 -# else -# define HAVE_GENERIC 0 -# endif -#endif -/* _Generic is buggy in pre-4.9 GCC. */ -#if !defined HAVE_GENERIC && defined __GNUC__ -# define HAVE_GENERIC (4 < __GNUC__ + (9 <= __GNUC_MINOR__)) -#endif -#ifndef HAVE_GENERIC -# define HAVE_GENERIC (201112 <= __STDC_VERSION__) -#endif - #if HAVE_GETTEXT # include <locale.h> /* for setlocale */ #endif /* HAVE_GETTEXT */ -- 2.37.3
* NEWS: Mention this. * zic.c (alignof): Rename from _Alignof, with backward compatibility substitute for pre-C23. --- NEWS | 6 ++++-- zic.c | 8 +++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/NEWS b/NEWS index ac8de47..be85fe4 100644 --- a/NEWS +++ b/NEWS @@ -8,6 +8,7 @@ Unreleased, experimental changes zic now supports links to links, and vanguard form uses this. Simplify four Ontario zones into one. Fix a Y2438 bug when reading TZif data. + In C code, use some C23 features if available. Changes to data @@ -55,8 +56,9 @@ Unreleased, experimental changes number 2438 comes from the 32-bit limit in the year 2038, plus the 400-year Gregorian cycle. (Problem reported by Bradley White.) - Take advantage of the following C23 features if available: - bool/true/false keywords, __has_include, unreachable. + In C code, prefer C23 keywords to pre-C23 macros for alignof, + bool, false, and true. Also, use the following C23 features if + available: __has_include, unreachable. Release 2022e - 2022-10-11 11:13:02 -0700 diff --git a/zic.c b/zic.c index a699285..e18506d 100644 --- a/zic.c +++ b/zic.c @@ -56,9 +56,11 @@ typedef int_fast64_t zic_t; static ptrdiff_t const PTRDIFF_MAX = MAXVAL(ptrdiff_t, TYPE_BIT(ptrdiff_t)); #endif -/* The minimum alignment of a type, for pre-C11 platforms. */ +/* The minimum alignment of a type, for pre-C23 platforms. */ #if __STDC_VERSION__ < 201112 -# define _Alignof(type) offsetof(struct { char a; type b; }, b) +# define alignof(type) offsetof(struct { char a; type b; }, b) +#elif __STDC_VERSION__ < 202311 +# include <stdalign.h> #endif /* The maximum length of a text line, including the trailing newline. */ @@ -2290,7 +2292,7 @@ writezone(const char *const name, const char *const string, char version, /* Allocate the ATS and TYPES arrays via a single malloc, as this is a bit faster. */ zic_t *ats = emalloc(align_to(size_product(nats, sizeof *ats + 1), - _Alignof(zic_t))); + alignof(zic_t))); void *typesptr = ats + nats; unsigned char *types = typesptr; struct timerange rangeall = {0}, range32, range64; -- 2.37.3
* zic.c (ZIC_MIN, ZIC_MAX, readlink, symlink): Now ordinary symbols instead of macros, as C macros are too powerful and not needed here. (link): Remove this macro; no longer needed. (linkat): Use HAVE_LINK directly. This should be a bit faster on systems that have symlinks but not hard links, since it avoids an unnecessary readlink call. --- zic.c | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/zic.c b/zic.c index e18506d..c549c85 100644 --- a/zic.c +++ b/zic.c @@ -23,8 +23,9 @@ #include <stdio.h> typedef int_fast64_t zic_t; -#define ZIC_MIN INT_FAST64_MIN -#define ZIC_MAX INT_FAST64_MAX +static zic_t const + ZIC_MIN = INT_FAST64_MIN, + ZIC_MAX = INT_FAST64_MAX; #define SCNdZIC SCNdFAST64 #ifndef ZIC_MAX_ABBR_LEN_WO_WARN @@ -137,17 +138,29 @@ extern char * optarg; extern int optind; #endif -#if ! HAVE_LINK -# define link(target, linkname) (errno = ENOTSUP, -1) -#endif #if ! HAVE_SYMLINK -# define readlink(file, buf, size) (errno = ENOTSUP, -1) -# define symlink(target, linkname) (errno = ENOTSUP, -1) +static ssize_t +readlink(char const *restrict file, char *restrict buf, size_t size) +{ + errno = ENOTSUP; + return -1; +} +static int +symlink(char const *target, char const *linkname) +{ + errno = ENOTSUP; + return -1; +} # define S_ISLNK(m) 0 #endif #ifndef AT_SYMLINK_FOLLOW -# define linkat(targetdir, target, linknamedir, linkname, flag) \ - (itssymlink(target) ? (errno = ENOTSUP, -1) : link(target, linkname)) +# if HAVE_LINK +# define linkat(targetdir, target, linknamedir, linkname, flag) \ + (itssymlink(target) ? (errno = ENOTSUP, -1) : link(target, linkname)) +# else +# define linkat(targetdir, target, linknamedir, linkname, flag) \ + (errno = ENOTSUP, -1) +# endif #endif static void addtt(zic_t starttime, int type); -- 2.37.3
* private.h (INT32_MAX, INT32_MIN): Don’t define substitutes; they don’t necessarily work on ones’ complement or signed-magnitude platforms. * zic.c (ZIC32_MIN, ZIC32_MAX): New constants, which work even on those oddball platforms. Use them instead of INT32_MIN, INT32_MAX. --- private.h | 7 ------- zic.c | 14 ++++++++------ 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/private.h b/private.h index 8024df5..c185d5f 100644 --- a/private.h +++ b/private.h @@ -369,13 +369,6 @@ typedef unsigned long uintmax_t; # endif #endif -#ifndef INT32_MAX -# define INT32_MAX 0x7fffffff -#endif /* !defined INT32_MAX */ -#ifndef INT32_MIN -# define INT32_MIN (-1 - INT32_MAX) -#endif /* !defined INT32_MIN */ - #ifndef SIZE_MAX # define SIZE_MAX ((size_t) -1) #endif diff --git a/zic.c b/zic.c index c549c85..3bbad94 100644 --- a/zic.c +++ b/zic.c @@ -25,7 +25,9 @@ typedef int_fast64_t zic_t; static zic_t const ZIC_MIN = INT_FAST64_MIN, - ZIC_MAX = INT_FAST64_MAX; + ZIC_MAX = INT_FAST64_MAX, + ZIC32_MIN = -1 - (zic_t) 0x7fffffff, + ZIC32_MAX = 0x7fffffff; #define SCNdZIC SCNdFAST64 #ifndef ZIC_MAX_ABBR_LEN_WO_WARN @@ -2398,7 +2400,7 @@ writezone(const char *const name, const char *const string, char version, max(hi_time, redundant_time - (ZIC_MIN < redundant_time)), ats, types); - range32 = limitrange(range64, INT32_MIN, INT32_MAX, ats, types); + range32 = limitrange(range64, ZIC32_MIN, ZIC32_MAX, ats, types); /* TZif version 4 is needed if a no-op transition is appended to indicate the expiration of the leap second table, or if the first @@ -2452,8 +2454,8 @@ writezone(const char *const name, const char *const string, char version, thisleapi = range32.leapbase; thisleapcnt = range32.leapcount; thisleapexpiry = range32.leapexpiry; - thismin = INT32_MIN; - thismax = INT32_MAX; + thismin = ZIC32_MIN; + thismax = ZIC32_MAX; } else { thisdefaulttype = range64.defaulttype; thistimei = range64.base; @@ -2486,7 +2488,7 @@ writezone(const char *const name, const char *const string, char version, /* Arguably the default time type in the 32-bit data should be range32.defaulttype, which is suited for - timestamps just before INT32_MIN. However, zic + timestamps just before ZIC32_MIN. However, zic traditionally used the time type of the indefinite past instead. Internet RFC 8532 says readers should ignore 32-bit data, so this discrepancy matters only @@ -2638,7 +2640,7 @@ writezone(const char *const name, const char *const string, char version, /* Output a LO_TIME transition if needed; see limitrange. But do not go below the minimum representable value for this pass. */ - lo = pass == 1 && lo_time < INT32_MIN ? INT32_MIN : lo_time; + lo = pass == 1 && lo_time < ZIC32_MIN ? ZIC32_MIN : lo_time; if (0 <= pretranstype) puttzcodepass(lo, fp, pass); -- 2.37.3
<https://endoflife.date/qt> says Qt 5.6, the last-supported Qt version to have the bug, reached end of security support on 2019-03-16. * NEWS: Mention this. * zic.c (WORK_AROUND_QTBUG_53071): Remove. All uses removed. --- NEWS | 5 +++++ zic.c | 34 +++------------------------------- 2 files changed, 8 insertions(+), 31 deletions(-) diff --git a/NEWS b/NEWS index be85fe4..bddbd3d 100644 --- a/NEWS +++ b/NEWS @@ -9,6 +9,7 @@ Unreleased, experimental changes Simplify four Ontario zones into one. Fix a Y2438 bug when reading TZif data. In C code, use some C23 features if available. + Remove no-longer-needed workaround for Qt bug 53071. Changes to data @@ -60,6 +61,10 @@ Unreleased, experimental changes bool, false, and true. Also, use the following C23 features if available: __has_include, unreachable. + zic no longer works around Qt bug 53071, as the relevant Qt + releases have been out of support since 2019. This change affects + only fat TZif files, as thin files never had the workaround. + Release 2022e - 2022-10-11 11:13:02 -0700 diff --git a/zic.c b/zic.c index 3bbad94..ad1221c 100644 --- a/zic.c +++ b/zic.c @@ -199,15 +199,6 @@ static zic_t tadd(zic_t t1, zic_t t2); /* Bound on length of what %z can expand to. */ enum { PERCENT_Z_LEN_BOUND = sizeof "+995959" - 1 }; -/* If true, work around a bug in Qt 5.6.1 and earlier, which mishandles - TZif files whose POSIX-TZ-style strings contain '<'; see - QTBUG-53071 <https://bugreports.qt.io/browse/QTBUG-53071>. This - workaround will no longer be needed when Qt 5.6.1 and earlier are - obsolete, say in the year 2021. */ -#ifndef WORK_AROUND_QTBUG_53071 -enum { WORK_AROUND_QTBUG_53071 = true }; -#endif - static int charcnt; static bool errors; static bool warnings; @@ -528,8 +519,7 @@ growalloc(void *ptr, size_t itemsize, ptrdiff_t nitems, ptrdiff_t *nitems_alloc) if (nitems < *nitems_alloc) return ptr; else { - ptrdiff_t nitems_max = PTRDIFF_MAX - WORK_AROUND_QTBUG_53071; - ptrdiff_t amax = min(nitems_max, SIZE_MAX); + ptrdiff_t amax = min(PTRDIFF_MAX, SIZE_MAX); if ((amax - 1) / 3 * 2 < *nitems_alloc) memory_exhausted(_("integer overflow")); *nitems_alloc += (*nitems_alloc >> 1) + 1; @@ -2298,17 +2288,14 @@ writezone(const char *const name, const char *const string, char version, register FILE * fp; register ptrdiff_t i, j; register int pass; - zic_t one = 1; - zic_t y2038_boundary = one << 31; - ptrdiff_t nats = timecnt + WORK_AROUND_QTBUG_53071; char *tempname = NULL; char const *outname = name; /* Allocate the ATS and TYPES arrays via a single malloc, as this is a bit faster. */ - zic_t *ats = emalloc(align_to(size_product(nats, sizeof *ats + 1), + zic_t *ats = emalloc(align_to(size_product(timecnt, sizeof *ats + 1), alignof(zic_t))); - void *typesptr = ats + nats; + void *typesptr = ats + timecnt; unsigned char *types = typesptr; struct timerange rangeall = {0}, range32, range64; @@ -2378,21 +2365,6 @@ writezone(const char *const name, const char *const string, char version, } } - /* Work around QTBUG-53071 for timestamps less than y2038_boundary - 1, - by inserting a no-op transition at time y2038_boundary - 1. - This works only for timestamps before the boundary, which - should be good enough in practice as QTBUG-53071 should be - long-dead by 2038. Do this after correcting for leap - seconds, as the idea is to insert a transition just before - 32-bit time_t rolls around, and this occurs at a slightly - different moment if transitions are leap-second corrected. */ - if (WORK_AROUND_QTBUG_53071 && timecnt != 0 && want_bloat() - && ats[timecnt - 1] < y2038_boundary - 1 && strchr(string, '<')) { - ats[timecnt] = y2038_boundary - 1; - types[timecnt] = types[timecnt - 1]; - timecnt++; - } - rangeall.defaulttype = defaulttype; rangeall.count = timecnt; rangeall.leapcount = leapcnt; -- 2.37.3
I see nothing in the C standard specification of qsort() that says sorting an array with zero members leads to undefined behaviour. http://port70.net/~nsz/c/c11/n1570.html#7.22.5.2 The POSIX specification for qsort() is quite clear that sorting zero elements is safe. https://pubs.opengroup.org/onlinepubs/9699919799/functions/qsort.html The manual pages for Linux and macOS (BSD) do not specify that sorting zero elements leads to undefined behaviour. Is there a platform where there is a known problem? If so, it would be worth documenting which platform/version explicitly so that the change can be undone in future if the recalcitrant platform(s) change their behaviour. With all that said, the change is trivial — but could be improved by using the test if (nlinks > 1) so that the code doesn't call qsort for a single-element array either; that is already in sorted order, of course. On Tue, Oct 25, 2022 at 10:10 PM Paul Eggert via tz <tz@iana.org> wrote:
* zic.c (make_links): Don't call qsort(NULL, 0, ...) as this has undefined behavior in C. This fixes a bug introduced five days ago. --- zic.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/zic.c b/zic.c index 78afa67..a699285 100644 --- a/zic.c +++ b/zic.c @@ -683,7 +683,8 @@ static void make_links(void) { ptrdiff_t i, j, nalinks; - qsort(links, nlinks, sizeof *links, qsort_linkcmp); + if (nlinks) + qsort(links, nlinks, sizeof *links, qsort_linkcmp);
/* Ignore each link superseded by a later link with the same name. */ j = 0; -- 2.37.3
-- Jonathan Leffler <jonathan.leffler@gmail.com> #include <disclaimer.h> Guardian of DBD::Informix - v2018.1031 - http://dbi.perl.org "Blessed are we who can laugh at ourselves, for we shall never cease to be amused."
Jonathan Leffler via tz <tz@iana.org> writes:
I see nothing in the C standard specification of qsort() that says sorting an array with zero members leads to undefined behaviour.
The C standard is silent on this (at least as of C99), but POSIX is absolutely unambiguous: If the nel argument has the value zero, the comparison function pointed to by compar shall not be called and no rearrangement shall take place.
With all that said, the change is trivial — but could be improved by using the test if (nlinks > 1) so that the code doesn't call qsort for a single-element array either; that is already in sorted order, of course.
Indeed --- testing for nlinks > 1 can be defended on performance grounds, whether or not you think you're dealing with a broken libc. I recall that Postgres used to carry some explicit checks to avoid calling bsearch() with zero elements, because the case was broken on nineties-vintage Solaris. I don't recall anyone ever claiming that any version of qsort() has such an issue. regards, tom lane
Tom Lane via tz said:
Jonathan Leffler via tz <tz@iana.org> writes:
I see nothing in the C standard specification of qsort() that says sorting an array with zero members leads to undefined behaviour.
The C standard is silent on this (at least as of C99), but POSIX is absolutely unambiguous:
If the nel argument has the value zero, the comparison function pointed to by compar shall not be called and no rearrangement shall take place.
But does it say whether the first argument is allowed to be NULL? -- Clive D.W. Feather | If you lie to the compiler, Email: clive@davros.org | it will get its revenge. Web: http://www.davros.org | - Henry Spencer Mobile: +44 7973 377646
On Oct 25, 2022, at 9:34 PM, Jonathan Leffler via tz <tz@iana.org> wrote:
I see nothing in the C standard specification of qsort() that says sorting an array with zero members leads to undefined behaviour.
I see nothing in C90 through C18 that explicitly say anything about passing NULL as base and 0 as nmemb. What I do see is The qsort function sorts an array of nmemb objects, the initial element of which is pointed to by base. The size of each object is specified by size. (from C18, but they all pretty much say the same thing). However, C18, at least, says An array type describes a contiguously allocated nonempty set of objects with a particular member object type, called the element type. The element type shall be complete whenever the array type is specified. Array types are characterized by their element type and by the number of elements in the array. An array type is said to be derived from its element type, and if its element type is T, the array type is sometimes called "array of T". The construction of an array type from an element type is called "array type derivation". emphasis on the "nonempty" There are cases where the array size can be omitted, such as extern int foo[]; or struct bar { int nelem; int elements[]; // flexible array member }; but, in the first case, I'm not sure "foo" could be zero-length (as it would have to be defined elsewhere, and you couldn't just have "int foo[0]"), and in the latter case, you could pass "elements" to qsort(), along with "nelem", but C18, at least, says of this case: As a special case, the last member of a structure with more than one named member may have an incomplete array type; this is called a flexible array member. In most situations, the flexible array member is ignored. In particular, the size of the structure is as if the flexible array member were omitted except that it may have more trailing padding than the omission would imply. However, when a . (or-> ) operator has a left operand that is (a pointer to) a structure with a flexible array member and the right operand names that member, it behaves as if that member were replaced with the longest array (with the same element type) that would not make the structure larger than the object being accessed; the offset of the array shall remain that of the flexible array member, even if this would differ from that of the replacement array. If this array would have no elements, it behaves as if it had one element but the behavior is undefined if any attempt is made to access that element or to generate a pointer one past it. so, in that case, a pointer is generated, but the results of trying to use that pointer are undefined. The specification for qsort() says nothing about whether, if "nmemb" is 0, any attempt is made to reference anything through "base".
The POSIX specification for qsort() is quite clear that sorting zero elements is safe.
https://pubs.opengroup.org/onlinepubs/9699919799/functions/qsort.html
If the nel argument has the value zero, the comparison function pointed to by compar shall not be called and no rearrangement shall take place. is a POSIX addition, not present in the C standard. (It probably *should* be in the C standard, but it isn't in anything between C90 and C18.) So, whilst I'd trust any POSIX-compliant system to handle qsort(NULL, 0, ...) sanely, and I'd trust the MSVC qsort() to handle it sanely based on https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/qsort?view... "This function validates its parameters. If compare or number is NULL, or if base is NULL and number is nonzero, or if width is less than zero, the invalid parameter handler is invoked, as described in Parameter validation." which suggests that they consider "base" being NULL and "number" being 0 to be valid and, presumably to be handled sanely, and given that the qsort() implementation is not guaranteed that any attempt to refer to anything pointed to by "base + i", for i >= nelem, will do anything nasty, thus meaning that if nelem == 0, that's "no guarantees for anything pointed to by base", I would *expect* sane behavior from all qsort() implementations. However, the C standard doesn't, as far as I can tell, *explicitly* require sanity in the nelem == 0 case, so maybe there's some weird implementation out there that looks at *base even if nelem == 0.
Guy Harris via tz <tz@iana.org> writes:
On Oct 25, 2022, at 9:34 PM, Jonathan Leffler via tz <tz@iana.org> wrote:
I see nothing in the C standard specification of qsort() that says sorting an array with zero members leads to undefined behaviour.
I see nothing in C90 through C18 that explicitly say anything about passing NULL as base and 0 as nmemb.
Refreshing my memory about this ... I think the argument hinges on this bit in C99, which applies to all C-specified library functions: 7.1.4 Use of library functions [#1] Each of the following statements applies unless explicitly stated otherwise in the detailed descriptions that follow: If an argument to a function has an invalid value (such as a value outside the domain of the function, or a pointer outside the address space of the program, or a null pointer) or a type (after promotion) not expected by a ^^^^^^^^^^^^ function with variable number of arguments, the behavior is undefined. If a function argument is described as being an array, the pointer actually passed to the function shall have a value such that all address computations and accesses to objects (that would be valid if the pointer did point to the first element of such an array) are in fact valid. There's a faction that thinks that the underlined comment entitles every libc function to halt-and-catch-fire when passed a null pointer. Never mind whether a nearby zero count argument clearly forbids it from making any memory accesses associated with that pointer, as expressed by the immediately following sentence. I side with Winston Churchill in saying "this is nonsense up with which I shall not put". There are no useful grounds for claiming that qsort, memset, memcpy, etc, with a null pointer and a zero count argument should be undefined. It's merely a gotcha for the unwary programmer. C had a similar gotcha back in the nineties for integer division with negative values ... which they eventually fixed. This needs to get fixed in the language standard, not worked around forevermore. regards, tom lane
On 26/10/2022 06:59, Tom Lane via tz wrote:
Guy Harris via tz <tz@iana.org> writes:
On Oct 25, 2022, at 9:34 PM, Jonathan Leffler via tz <tz@iana.org> wrote:
I see nothing in the C standard specification of qsort() that says sorting an array with zero members leads to undefined behaviour.
I see nothing in C90 through C18 that explicitly say anything about passing NULL as base and 0 as nmemb.
Refreshing my memory about this ... I think the argument hinges on this bit in C99, which applies to all C-specified library functions:
7.1.4 Use of library functions
[#1] Each of the following statements applies unless explicitly stated otherwise in the detailed descriptions that follow: If an argument to a function has an invalid value (such as a value outside the domain of the function, or a pointer outside the address space of the program, or a null pointer) or a type (after promotion) not expected by a ^^^^^^^^^^^^ function with variable number of arguments, the behavior is undefined. If a function argument is described as being an array, the pointer actually passed to the function shall have a value such that all address computations and accesses to objects (that would be valid if the pointer did point to the first element of such an array) are in fact valid.
Also in 7.22.5 Searching and sorting utilities, paragraph 1: [...] Pointer arguments on such a call shall still have valid values, as described in 7.1.4.
There's a faction that thinks that the underlined comment entitles every libc function to halt-and-catch-fire when passed a null pointer. Never mind whether a nearby zero count argument clearly forbids it from making any memory accesses associated with that pointer, as expressed by the immediately following sentence.
I side with Winston Churchill in saying "this is nonsense up with which I shall not put". There are no useful grounds for claiming that qsort, memset, memcpy, etc, with a null pointer and a zero count argument should be undefined. It's merely a gotcha for the unwary programmer. C had a similar gotcha back in the nineties for integer division with negative values ... which they eventually fixed. This needs to get fixed in the language standard, not worked around forevermore.
I tend to agree, and was not really aware of the requirement until today! -- -=( Ian Abbott <abbotti@mev.co.uk> || MEV Ltd. is a company )=- -=( registered in England & Wales. Regd. number: 02862268. )=- -=( Regd. addr.: S11 & 12 Building 67, Europa Business Park, )=- -=( Bird Hall Lane, STOCKPORT, SK3 0XA, UK. || www.mev.co.uk )=-
On 10/26/22 01:58, Ian Abbott via tz wrote:
Also in 7.22.5 Searching and sorting utilities, paragraph 1:
[...] Pointer arguments on such a call shall still have valid values, as described in 7.1.4.
Yes, that's why portable C code can't call qsort(NULL, 0, ...). It's not just an academic point. Without the patch, 'zic /dev/null' dumps core on Fedora 36 x86-64, when tzcode is built via "make CFLAGS='$(GCC_DEBUG_FLAGS)'". That's how I discovered the need for the patch. The core dump occurred because GCC translates this: qsort(links, nlinks, sizeof *links, qsort_linkcmp); as if it were this: if (nlinks == 0) __builtin_trap(); qsort(links, nlinks, sizeof *links, qsort_linkcmp); That is, if qsort's second argument is zero, the code generated by GCC doesn't call the qsort library function. Instead, it directly executes the ud2 instruction <https://www.felixcloutier.com/x86/ud>, which raises the invalid opcode exception. Presumably this is because the GCC maintainers are in the faction that says a null pointer cannot be used to pass a size-zero object to a library function. This is likely the same faction that says "char *p = NULL; return p + 0;" has undefined behavior.
Paul Eggert wrote:
It's not just an academic point. Without the patch, 'zic /dev/null' dumps core on Fedora 36 x86-64... The core dump occurred because GCC translates this: qsort(links, nlinks, sizeof *links, qsort_linkcmp); as if it were this:
if (nlinks == 0) __builtin_trap(); qsort(links, nlinks, sizeof *links, qsort_linkcmp);
This is an astonishing result. It's hard to imagine it's worthwhile for gcc to perform this "optimization". There are some decent resident language lawyers over on Stack Overflow these days. I asked this question there, and several posters have cited language from C17 which seems (like the Posix language others have mentioned here) to directly contraindicate gcc's behavior: These utilities [qsort, bsearch] make use of a comparison function to search or sort arrays of unspecified type. Where an argument declared as size_t nmemb specifies the length of the array for a function, nmemb can have the value zero on a call to that function; the comparison function is not called, a search finds no matching element, and sorting performs no rearrangement. Pointer arguments on such a call shall still have valid values, as described in 7.1.4. [C17 Sec. 7.22.5] https://stackoverflow.com/questions/74207802/qsort-with-size-0 THe only explanation I can think of is that gcc somehow knows that it is calling a recursive `qsort` implementation where the caller is responsible for terminating the recursion -- but even then, surely the behavior on n == 0 should be to do nothing (that is, as if void qsort() had returned immediately) rathern than to gratuitiously abort! Paul, which version of gcc is this, and what is $(GCC_DEBUG_FLAGS) expanding to?
Steve Summit via tz said:
There are some decent resident language lawyers over on Stack Overflow these days. I asked this question there, and several posters have cited language from C17 which seems (like the Posix language others have mentioned here) to directly contraindicate gcc's behavior:
These utilities [qsort, bsearch] make use of a comparison function to search or sort arrays of unspecified type. Where an argument declared as size_t nmemb specifies the length of the array for a function, nmemb can have the value zero on a call to that function; the comparison function is not called, a search finds no matching element, and sorting performs no rearrangement. Pointer arguments on such a call shall still have valid values, as described in 7.1.4.
[C17 Sec. 7.22.5]
That text was added in C99; it wasn't in C90. I think it might have been my idea; it certainly feels familiar. -- Clive D.W. Feather | If you lie to the compiler, Email: clive@davros.org | it will get its revenge. Web: http://www.davros.org | - Henry Spencer Mobile: +44 7973 377646
On 2022-10-26 07:09, Steve Summit via tz wrote:
The core dump occurred because GCC translates this: qsort(links, nlinks, sizeof *links, qsort_linkcmp); as if it were this:
if (nlinks == 0) __builtin_trap(); qsort(links, nlinks, sizeof *links, qsort_linkcmp); This is an astonishing result. It's hard to imagine it's worthwhile for gcc to perform this "optimization".
Oh, sorry, it's astonishing because I misread the assembly language that GCC generates. The actual translation looks at links, not nlinks. That is, it's translated as if it were: if (links == NULL) __builtin_trap(); qsort(links, nlinks, sizeof *links, qsort_linkcmp); Sorry about the confusion.
Paul, which version of gcc is this, and what is $(GCC_DEBUG_FLAGS) expanding to?
It's GCC 12.2.1 20220819 (Red Hat 12.2.1-2). $(GCC_DEBUG_FLAGS) is the default in the Makefile, which expands to the long string at the end of this email. The key option here is -fsanitize=undefined, which causes GCC to insert extra checking code to trap in many (but not all) places when behavior is undefined; this explains why GCC pessimized the code here. GCC generates the ud2 instruction because $(GCC_DEBUG_FLAGS) also contains -fsanitize-undefined-trap-on-error. I use this option, despite the hassle it causes when debugging, because it means I don't have to worry about libubsan which can be a configuration problem. I use these debugging options partly to check tzcode's portability. Although qsort(NULL, 0, ...) works fine on most practical platforms, as Clive noted there is (or was) an oddball platform or two that is (or was) verrrry picky about pointers, and for a program like zic where portability is more important than performance, it's nice if zic runs even on oddballs. Clive, do you happen to know what these platforms are (or were), and whether they're still supported? As a practical matter, even though the C standard says that expressions like qsort(NULL, 0, ...) and (char *)NULL + 0 have undefined behavior, any new platform would be verrrry wise to define them to work the same way that most practical platforms do; this is true regardless of what the C standard says. And to some extent this means the C standard is not at the sweetest spot it could be in, as a contract between implementers and programmers. I write and see a lot of code where adding 0 to NULL is expected to yield NULL, and nobody thinks twice about it (nor should they). Here's what $(GCC_DEBUG_FLAGS) expands to in the default build: -DTZDIR='"/usr/share/zoneinfo"' -DGCC_LINT -g3 -O3 -fno-common -fsanitize=undefined -fsanitize-address-use-after-scope -fsanitize-undefined-trap-on-error -fstack-protector -Wall -Wextra -Walloc-size-larger-than=100000 -Warray-bounds=2 -Wbad-function-cast -Wbidi-chars=any,ucn -Wcast-align=strict -Wdate-time -Wdeclaration-after-statement -Wdouble-promotion -Wduplicated-branches -Wduplicated-cond -Wformat=2 -Wformat-overflow=2 -Wformat-signedness -Wformat-truncation -Winit-self -Wlogical-op -Wmissing-declarations -Wmissing-prototypes -Wnested-externs -Wnull-dereference -Wold-style-definition -Woverlength-strings -Wpointer-arith -Wshadow -Wshift-overflow=2 -Wstrict-overflow -Wstrict-prototypes -Wstringop-overflow=4 -Wstringop-truncation -Wsuggest-attribute=cold -Wsuggest-attribute=const -Wsuggest-attribute=format -Wsuggest-attribute=malloc -Wsuggest-attribute=noreturn -Wsuggest-attribute=pure -Wtrampolines -Wundef -Wuninitialized -Wunused-macros -Wuse-after-free=3 -Wvariadic-macros -Wvla -Wwrite-strings -Wno-address -Wno-format-nonliteral -Wno-sign-compare -Wno-type-limits -Wno-unused-parameter
Paul Eggert said:
I use these debugging options partly to check tzcode's portability. Although qsort(NULL, 0, ...) works fine on most practical platforms, as Clive noted there is (or was) an oddball platform or two that is (or was) verrrry picky about pointers, and for a program like zic where portability is more important than performance, it's nice if zic runs even on oddballs. Clive, do you happen to know what these platforms are (or were), and whether they're still supported?
Sorry, but I have no idea. We didn't always get told - if someone at the meeting said "I know an platform that does X", we believed them. There was a longstanding meme in the meetings (and on comp.std.c) that "if you don't understand the reasoning behing something in the C Standard, the answer is the IBM AS400".
As a practical matter, even though the C standard says that expressions like qsort(NULL, 0, ...) and (char *)NULL + 0 have undefined behavior, any new platform would be verrrry wise to define them to work the same way that most practical platforms do; this is true regardless of what the C standard says. And to some extent this means the C standard is not at the sweetest spot it could be in, as a contract between implementers and programmers. I write and see a lot of code where adding 0 to NULL is expected to yield NULL, and nobody thinks twice about it (nor should they).
Why not? You're assuming that NULL is represented by 32 or 64 zero bits [1]. That's not what all computers do. Unusual architectures may do something completely different and they might well trap on trying to add to a null pointer, irrespective of the value you're adding. This is, indeed, a modern version of the "all the world's a Vax" fallacy. [1] For those not used to this particular discussion, an integer *constant* that evaluates to zero, or one of those cast to a pointer type, is how you represent a null pointer. But that does not mean that: #define UCPSIZE (sizeof (unsigned char *)) union { unsigned char *p; char c [UCPSIZE]; } u; u.p = NULL; unsigned char z = 0; for (unsigned int i = 0; i < UCPSIZE; i++) z |= u.c [i]; printf ("Result = %u\n", z); is required to print zero. On the contrary, the representation of a null pointer is completely unspecified. -- Clive D.W. Feather | If you lie to the compiler, Email: clive@davros.org | it will get its revenge. Web: http://www.davros.org | - Henry Spencer Mobile: +44 7973 377646
<<On Thu, 27 Oct 2022 07:38:42 +0100, "Clive D.W. Feather via tz" <tz@iana.org> said:
Why not? You're assuming that NULL is represented by 32 or 64 zero bits [1]. That's not what all computers do. Unusual architectures may do something completely different
I don't want to speak for Paul, but I think it is a true statement that such an "unusual architecture" would be unsaleable today. Even CHERI uses all-bits-zero-and-untagged as a null pointer. -GAWollman
On Oct 27, 2022, at 8:47 AM, Garrett Wollman via tz <tz@iana.org> wrote:
<<On Thu, 27 Oct 2022 07:38:42 +0100, "Clive D.W. Feather via tz" <tz@iana.org> said:
Why not? You're assuming that NULL is represented by 32 or 64 zero bits [1]. That's not what all computers do. Unusual architectures may do something completely different
I don't want to speak for Paul, but I think it is a true statement that such an "unusual architecture" would be unsaleable today.
I don't know whether the AS/400 systems represent null pointers in an unusual fashion (although I wouldn't be surprised if they do), but IBM still sells systems running the same operating system as the AS/400 (including the low-level "System Licensed Internal Code" system software, originally called "vertical microcode" in System/38 in order to convince courts that would enforce a feared future antitrust action that it's not software that IBM needs to offer to clone makers): https://www.ibm.com/it-infrastructure/power/os/ibm-i S/38, AS/400. and LPARs running IBM i on an IBM Power Systems machine are... somewhat odd systems. The nominal target for compilers is https://www.ibm.com/docs/en/i/7.3?topic=interfaces-i-machine-interface which is the current version of http://bitsavers.org/pdf/ibm/system38/GA21-9331-1_System_38_Functional_Refer... http://bitsavers.org/pdf/ibm/system38/GA21-9330-4_IBM_System_38_Functional_C... (for which there was no C implementation - that first showed up in AS/400). However, that instruction set is not directly executed; instead, a component of the "vertical microcode"/"System Licensed Internal Code", all of which runs in main memory, translates that "machine interface" code into machine code, and *that's* what's run. The machine code instruction set - which is also the machine code instruction set into which the "vertical microcode"/"System Licensed Internal Code" source is compiled/assembled - is currently Power ISA plus some IBM I-specific extensions, including https://www.devever.net/~hl/ppcas plus some other instructions such as decimal arithmetic helpers, some of which may now be documented parts of Power ISA, and was originally a somewhat oddball instruction set: http://bitsavers.org/pdf/ibm/system38/SC21-9037-3_IBM_System_38_Internal_Mic... in System/38 (and some variant of which was probably the machine code instruction set in the CISC AS/400 models). That instruction set was implemented in what would generally be considered by microcode on various S/38 and AS/400 models; that was the "horizontal microcode" on those machines. And there are C and C++ compilers for IBM i: https://www.ibm.com/docs/en/i/7.4?topic=c-ile-cc-compiler-reference There's also, presumably, some (possibly internal-only) version of the C++ compiler for Power ISA targets that is used for most of the "System Licensed Internal Code" for RISC AS/400 and IBM Power Systems IBM i, as that's mostly written in C++. (I think the original "vertical microcode" for System/38 and AS/400 was written in one of IBM's many PL/I-derived internal languages, this one being named PL/MP for "Programming Language/Machine Product".)
On 2022-10-26 23:38, Clive D.W. Feather wrote:
You're assuming that NULL is represented by 32 or 64 zero bits [1].
No, I'm not assuming that. I'm aware of the AS/400 and its successor IBM i, whose C compiler ILE C does not use an all-bits-zero representation for null pointers. But as I understand it, on this legacy platform a pointer is internally represented by a memory space reference plus an offset, and adding 0 to a null pointer (using the ADDSPP instruction[1]) does not modify the offset and still gives you a null pointer. So I am unaware of any platform where (char *)0 + 0 does not give you a null pointer. If I'm right, the C standard's prohibition of (char *)0 + 0 is unnecessarily restrictive, as it prohibits programs that are portable in practice. It wouldn't be the first time that a standard is unnecessarily restrictive. Luckily, when writing free software we're free to ignore standards when they're wrong.... [1] https://www.ibm.com/docs/en/i/7.5?topic=instructions-add-space-pointer-addsp...
On 2022-10-27 10:41, Paul Eggert via tz wrote:
On 2022-10-26 23:38, Clive D.W. Feather wrote:
You're assuming that NULL is represented by 32 or 64 zero bits [1].
No, I'm not assuming that. I'm aware of the AS/400 and its successor IBM i, whose C compiler ILE C does not use an all-bits-zero representation for null pointers. But as I understand it, on this legacy platform a pointer is internally represented by a memory space reference plus an offset, and adding 0 to a null pointer (using the ADDSPP instruction[1]) does not modify the offset and still gives you a null pointer. So I am unaware of any platform where (char *)0 + 0 does not give you a null pointer.
If I'm right, the C standard's prohibition of (char *)0 + 0 is unnecessarily restrictive, as it prohibits programs that are portable in practice.
It wouldn't be the first time that a standard is unnecessarily restrictive. Luckily, when writing free software we're free to ignore standards when they're wrong....
[1] https://www.ibm.com/docs/en/i/7.5?topic=instructions-add-space-pointer-addsp...
Itanium NULL pointer representation is -1: https://refspecs.linuxfoundation.org/cxxabi-1.75.html See 2.3 Member pointers, which also states pointer value zero is NULL. -- Take care. Thanks, Brian Inglis Calgary, Alberta, Canada La perfection est atteinte Perfection is achieved non pas lorsqu'il n'y a plus rien à ajouter not when there is no more to add mais lorsqu'il n'y a plus rien à retirer but when there is no more to cut -- Antoine de Saint-Exupéry
On 10/27/22 15:53, Brian Inglis via tz wrote:
Itanium NULL pointer representation is -1:
That's talking about null data member pointers, not null pointers. Data member pointers are a C++ concept and are different animals from pointers. Some C++ implementations represent null data member pointers with 0, some with -1 (both ideas work reasonably well). Either way, adding 0 to one gives you the same data member pointer you started off with, namely, a null data member pointer (not that this matters for C).
Paul Eggert said:
So I am unaware of any platform where (char *)0 + 0 does not give you a null pointer.
If I'm right, the C standard's prohibition of (char *)0 + 0 is unnecessarily restrictive, as it prohibits programs that are portable in practice.
The problem is that "I am unaware of any" is not the same as "there is no". A lot of people don't believe me when I say that there is a processor where CHAR_BIT is 16 and sizeof(int) is 1. Yet for many years it was the third most common processor in the world. I'm no longer involved in WG14 and haven't been for many years. You'd have to raise it with them. -- Clive D.W. Feather | If you lie to the compiler, Email: clive@davros.org | it will get its revenge. Web: http://www.davros.org | - Henry Spencer Mobile: +44 7973 377646
Paul Eggert <eggert@cs.ucla.edu> writes:
The core dump occurred because GCC translates this:
qsort(links, nlinks, sizeof *links, qsort_linkcmp);
as if it were this:
if (nlinks == 0) __builtin_trap(); qsort(links, nlinks, sizeof *links, qsort_linkcmp);
Right, so they are actually going out of their way to make it a gotcha. What point does that serve? regards, tom lane
On 26/10/2022 11:03, Paul Eggert wrote:
The core dump occurred because GCC translates this:
qsort(links, nlinks, sizeof *links, qsort_linkcmp);
as if it were this:
if (nlinks == 0) __builtin_trap(); qsort(links, nlinks, sizeof *links, qsort_linkcmp);
That is, if qsort's second argument is zero, the code generated by GCC doesn't call the qsort library function. Instead, it directly executes the ud2 instruction <https://www.felixcloutier.com/x86/ud>, which raises the invalid opcode exception. Presumably this is because the GCC maintainers are in the faction that says a null pointer cannot be used to pass a size-zero object to a library function. This is likely the same faction that says "char *p = NULL; return p + 0;" has undefined behavior.
Does it also call __builtin_trap() if nlinks is 0 and links is a valid pointer to modifiable storage? Because it should allow nlinks to be 0 in that case, according to the C standard. -- -=( Ian Abbott <abbotti@mev.co.uk> || MEV Ltd. is a company )=- -=( registered in England & Wales. Regd. number: 02862268. )=- -=( Regd. addr.: S11 & 12 Building 67, Europa Business Park, )=- -=( Bird Hall Lane, STOCKPORT, SK3 0XA, UK. || www.mev.co.uk )=-
On Wed, Oct 26, 2022 at 4:03 AM Paul Eggert via tz <tz@iana.org> wrote:
The core dump occurred because GCC translates this:
qsort(links, nlinks, sizeof *links, qsort_linkcmp);
as if it were this:
if (nlinks == 0) __builtin_trap(); qsort(links, nlinks, sizeof *links, qsort_linkcmp);
That is, if qsort's second argument is zero, the code generated by GCC doesn't call the qsort library function. Instead, it directly executes the ud2 instruction <https://www.felixcloutier.com/x86/ud>, which raises the invalid opcode exception. Presumably this is because the GCC maintainers are in the faction that says a null pointer cannot be used to pass a size-zero object to a library function. This is likely the same faction that says "char *p = NULL; return p + 0;" has undefined behavior.
It seems odd that GCC is testing the counter nlinks and not the pointer links. Passing a null pointer is invalid — §7.10.5, §7.10.5.2, §7.1.4 of the C standard (C99 or later) indicate that. But passing nlinks as zero should not cause any trouble according to C99 or later versions of the C standard, or according to POSIX. The C90 standard is silent on the issue of a zero count. However, I think that TZ code can sidestep the whole issue by using: if (nlinks > 1) qsort(links, nlinks, sizeof(*links), qsort_linkcmp); There's no need to sort arrays of size 0 or 1. This avoids any questions about whether a count of zero is valid as an argument to qsort(). -- Jonathan Leffler <jonathan.leffler@gmail.com> #include <disclaimer.h> Guardian of DBD::Informix - v2018.1031 - http://dbi.perl.org "Blessed are we who can laugh at ourselves, for we shall never cease to be amused."
On Wed, Oct 26, 2022 at 8:54 AM Jonathan Leffler <jonathan.leffler@gmail.com> wrote:
However, I think that TZ code can sidestep the whole issue by using:
if (nlinks > 1) qsort(links, nlinks, sizeof(*links), qsort_linkcmp);
There's no need to sort arrays of size 0 or 1. This avoids any questions about whether a count of zero is valid as an argument to qsort().
Of course, I assume here that links will never be null when nlinks is 1 or more — the only time links could possibly be null is if nlinks is 0. If there are other circumstances where links could be null, you could add to the condition: if (nlinks > 1 && links != NULL) qsort(links, nlinks, sizeof(*links), qsort_linkcmp); -- Jonathan Leffler <jonathan.leffler@gmail.com> #include <disclaimer.h> Guardian of DBD::Informix - v2018.1031 - http://dbi.perl.org "Blessed are we who can laugh at ourselves, for we shall never cease to be amused."
On 2022-10-26 07:54, Jonathan Leffler wrote:
There's no need to sort arrays of size 0 or 1.
Good point, thanks. I installed the first attached patch to do that. The second patch just fixes spacing as a style matter. Although there's a similar issue with bsearch and arrays with zero members, I didn't add a size-zero check there, as the base pointer must be nonnull when bsearch is called, the C standard says that bsearch does the right thing if you give it a valid non-null pointer and a zero nmemb, and I didn't see much point to adding an optimization there. Whereas with qsort, the base pointer might be null so the code must check for either a null pointer or size zero anyway, so why not check for size 1 as that's nearly as cheap and arguably clearer?
Paul Eggert via tz said:
Presumably this is because the GCC maintainers are in the faction that says a null pointer cannot be used to pass a size-zero object to a library function. This is likely the same faction that says "char *p = NULL; return p + 0;" has undefined behavior.
I agree with them in both cases. There are no zero-sized objects in C. (This is C90 and C99; I dropped off WG14 in the early noughties.) -- Clive D.W. Feather | If you lie to the compiler, Email: clive@davros.org | it will get its revenge. Web: http://www.davros.org | - Henry Spencer Mobile: +44 7973 377646
Tom Lane via tz said:
7.1.4 Use of library functions
[#1] Each of the following statements applies unless explicitly stated otherwise in the detailed descriptions that follow: If an argument to a function has an invalid value (such as a value outside the domain of the function, or a pointer outside the address space of the program, or a null pointer) or a type (after promotion) not expected by a ^^^^^^^^^^^^ function with variable number of arguments, the behavior is undefined. If a function argument is described as being an array, the pointer actually passed to the function shall have a value such that all address computations and accesses to objects (that would be valid if the pointer did point to the first element of such an array) are in fact valid.
There's a faction that thinks that the underlined comment entitles every libc function to halt-and-catch-fire when passed a null pointer.
They're right.
Never mind whether a nearby zero count argument clearly forbids it from making any memory accesses associated with that pointer, as expressed by the immediately following sentence.
No, because the implementation might do something like: endptr = (unsigned char *) base + nmemb * size; before (or after) checking the value of nmemb.
I side with Winston Churchill in saying "this is nonsense up with which I shall not put".
Actually, he didn't say it.
There are no useful grounds for claiming that qsort, memset, memcpy, etc, with a null pointer and a zero count argument should be undefined.
There are, or were, implementers who disagree with you. -- Clive D.W. Feather | If you lie to the compiler, Email: clive@davros.org | it will get its revenge. Web: http://www.davros.org | - Henry Spencer Mobile: +44 7973 377646
<<On Wed, 26 Oct 2022 01:59:40 -0400, Tom Lane via tz <tz@iana.org> said:
I side with Winston Churchill in saying "this is nonsense up with which I shall not put". There are no useful grounds for claiming that qsort, memset, memcpy, etc, with a null pointer and a zero count argument should be undefined.
In a perhaps less ridiculous case, undefined behavior when such a library function is called with a null pointer for a source argument allows the implementation (especially an inlining compiler) to pre-warm the d-cache before entering the main loop. Not all architectures have a reliably non-trapping speculative read operation, so this gives the compiler a bit more freedom and allows it to inline a higher-performance implementation without first having to prove that the source pointer isn't null. This behavior seems silly for qsort() but it seems likely that the compiler will share the same annotations and implementation strategy for all standard library functions. -GAWollman
On 2022-10-26 11:55, Garrett Wollman via tz wrote:
In a perhaps less ridiculous case, undefined behavior when such a library function is called with a null pointer for a source argument allows the implementation (especially an inlining compiler) to pre-warm the d-cache before entering the main loop.
Unless I'm misunderstanding your example this sort of optimization cannot be done for qsort, since the base pointer can be valid and non-null but not dereferencable, e.g., qsort has well-defined behavior if the pointer points one past the end of an array and nmemb is zero.
Guy Harris via tz said:
However, C18, at least, says
An array type describes a contiguously allocated nonempty set of objects with a particular member object type, called the element type. The element type shall be complete whenever the array type is specified. Array types are characterized by their element type and by the number of elements in the array. An array type is said to be derived from its element type, and if its element type is T, the array type is sometimes called "array of T". The construction of an array type from an element type is called "array type derivation".
emphasis on the "nonempty"
In all my time in WG14 (i.e. early 90s to early noughties), we were clear that there were no zero-sized objects in C.
There are cases where the array size can be omitted, such as extern int foo[];
or
struct bar { int nelem; int elements[]; // flexible array member };
but, in the first case, I'm not sure "foo" could be zero-length (as it would have to be defined elsewhere, and you couldn't just have "int foo[0]"),
Exactly.
and in the latter case, you could pass "elements" to qsort(), along with "nelem", but C18, at least, says of this case: [...]
That's a special case that I got added to C99 to handle a common usage that we'd just ruled undefined. For the full story, see these two WG14 working papers: http://www.davros.org/c/wg14n791.txt http://www.davros.org/c/wg14n861.txt (item 2) -- Clive D.W. Feather | If you lie to the compiler, Email: clive@davros.org | it will get its revenge. Web: http://www.davros.org | - Henry Spencer Mobile: +44 7973 377646
participants (9)
-
Brian Inglis -
Clive D.W. Feather -
Garrett Wollman -
Guy Harris -
Ian Abbott -
Jonathan Leffler -
Paul Eggert -
scs@eskimo.com -
Tom Lane