RHEL Engineering is moving the tracking of its product development work on RHEL 6 through RHEL 9 to Red Hat Jira (issues.redhat.com). If you're a Red Hat customer, please continue to file support cases via the Red Hat customer portal. If you're not, please head to the "RHEL project" in Red Hat Jira and file new tickets here. Individual Bugzilla bugs in the statuses "NEW", "ASSIGNED", and "POST" are being migrated throughout September 2023. Bugs of Red Hat partners with an assigned Engineering Partner Manager (EPM) are migrated in late September as per pre-agreed dates. Bugs against components "kernel", "kernel-rt", and "kpatch" are only migrated if still in "NEW" or "ASSIGNED". If you cannot log in to RH Jira, please consult article #7032570. That failing, please send an e-mail to the RH Jira admins at rh-issues@redhat.com to troubleshoot your issue as a user management inquiry. The email creates a ServiceNow ticket with Red Hat. Individual Bugzilla bugs that are migrated will be moved to status "CLOSED", resolution "MIGRATED", and set with "MigratedToJIRA" in "Keywords". The link to the successor Jira issue will be found under "Links", have a little "two-footprint" icon next to it, and direct you to the "RHEL project" in Red Hat Jira (issue links are of type "https://issues.redhat.com/browse/RHEL-XXXX", where "X" is a digit). This same link will be available in a blue banner at the top of the page informing you that that bug has been migrated.
Bug 2173624 - -flto=auto causes gfs2-utils unit test failures
Summary: -flto=auto causes gfs2-utils unit test failures
Keywords:
Status: CLOSED WONTFIX
Alias: None
Product: Red Hat Enterprise Linux 9
Classification: Red Hat
Component: gcc
Version: 9.3
Hardware: Unspecified
OS: Unspecified
unspecified
unspecified
Target Milestone: rc
: ---
Assignee: Marek Polacek
QA Contact: qe-baseos-tools-bugs
URL:
Whiteboard:
Depends On:
Blocks:
TreeView+ depends on / blocked
 
Reported: 2023-02-27 14:34 UTC by Andrew Price
Modified: 2023-07-18 14:25 UTC (History)
7 users (show)

Fixed In Version:
Doc Type: No Doc Update
Doc Text:
Clone Of:
Environment:
Last Closed: 2023-03-30 07:29:59 UTC
Type: Bug
Target Upstream Version:
Embargoed:


Attachments (Terms of Use)


Links
System ID Private Priority Status Summary Last Updated
Red Hat Issue Tracker RHELPLAN-149934 0 None None None 2023-02-27 14:36:40 UTC

Description Andrew Price 2023-02-27 14:34:16 UTC
Description of problem:

Building gfs2-utils for cs9, a unit test fails at a basic endianness conversion check. On closer inspection I found that -flto=auto was to blame.

The rawhide package builds with -flto=auto and the unit test passes there, and building with CC=clang and the same CFLAGS works fine. So it seems that this is specific to gcc in cs9/rhel9.

Version-Release number of selected component (if applicable):
gcc-11.3.1-4.3.el9.x86_64

How reproducible:
100%

Steps to Reproduce:
1. yum install ncurses-devel kernel-headers automake libtool zlib-devel gettext-devel bison flex libblkid-devel libuuid-devel check-devel bzip2-devel git
2. git clone https://pagure.io/gfs2-utils.git
3. cd gfs2-utils
4. ./autogen.sh && ./configure CFLAGS=-flto=auto
5. make -C gfs2/libgfs2 check_libgfs2
6. gfs2/libgfs2/check_libgfs2

(To run check_libgfs2 under gdb sensibly requires the CK_FORK=no environment variable.)

Actual results:
check_ondisk.c:79:F:On-disk structure parsing checks:check_sb1_out:0: Assertion '__bswap_32 (( uint32_t)(__be32)(sb->sb_fs_format)) == 0x5a5a5a51' failed

Expected results:
Tests pass

Additional info:

The gfs2-utils package hasn't been updated in a while so the issue might not have been introduced recently. We don't build with -flto=auto in our upstream CI testing.

The latest Fedora builds use -flto=auto and they haven't been failing: https://koji.fedoraproject.org/koji/packageinfo?packageID=291

This is the check that fails: https://pagure.io/gfs2-utils/blob/5166bdbda096ce8efc186beb5efb1e08045b5ffe/f/gfs2/libgfs2/check_ondisk.c#_79

(In case the link breaks it's gfs2/libgfs2/check_ondisk.c:79 in the gfs2-utils 3.5.0 release tarball.)

Comment 1 Marek Polacek 2023-03-01 21:19:33 UTC
FWIW, I can reproduce the fail with system RHEL 9 gcc but it works with GTS 12 gcc.

Comment 2 Marek Polacek 2023-03-15 22:30:13 UTC
It works with GTS 12 gcc because the problem went away with https://gcc.gnu.org/git/?p=gcc.git;a=commitdiff;h=2b8453c401b699ed93c085d0413ab4b5030bcdb8
but that means that with -fno-tree-loop-vectorize -fno-tree-slp-vectorize the problem reappears.  But I notice that
it works with -fno-strict-aliasing.  -Wstrict-aliasing=2 warns about:

rgrp.c: In function ‘lgfs2_rgrp_bitbuf_alloc’:
rgrp.c:121:37: warning: dereferencing type-punned pointer might break strict-aliasing rules [-Wstrict-aliasing]
  121 |         if (posix_memalign((void **)&bufs, io_align, len) != 0) {
      |                                     ^~~~~

I'm not sure if that is the bug but I think there's a bug in the package.

Comment 3 Andrew Price 2023-03-16 12:22:41 UTC
Fixing up the strict-aliasing warning doesn't fix the original problem but thanks for highlighting it.

Comment 4 Marek Polacek 2023-03-16 18:03:57 UTC
Looks like this is -fipa-modref.  diff w/ and w/o __attribute__((optimize("no-ipa-modref")))
on
__attribute__((optimize("no-ipa-modref")))
void lgfs2_sb_out(const struct lgfs2_sbd *sdp, void *buf)
{
...
}

@@ -4417,214 +4398,13 @@ void check_sb1_out_fn (int _i)
   sbd.sd_license_di.in_addr = 6510615555426900560;
   MEM <unsigned long> [(unsigned int *)&sbd + 96B] = 6510615538247031381;
   lgfs2_sb_out (&sbd, &buf);
-  _1 = MEM[(struct gfs_sb *)&buf].sb_fs_format;
-  if (_1 == 1364875866)
-    goto <bb 3>; [99.96%]
-  else
-    goto <bb 4>; [0.04%]
-
-  <bb 3> [local count: 1073312329]:
-  _mark_point ("check_ondisk.c", 80);
-  _2 = MEM[(struct gfs_sb *)&buf].sb_multihost_format;
-  if (_2 == 1381653082)
-    goto <bb 5>; [99.96%]
-  else
-    goto <bb 6>; [0.04%]
-
-  <bb 4> [local count: 429496]:
   _ck_assert_failed ("check_ondisk.c", 80, "Assertion \'__bswap_32 (( uint32_t)(__be32)(sb->sb_fs_format)) == 0x5a5a5a51\' failed", 0B);

I guess due to modref we think that the lgfs2_sb_out call doesn't change buf and so _1 == 1364875866 must be false?

This fixes it:

--- libgfs2.h.mp	2023-03-16 13:54:09.387914303 -0400
+++ libgfs2.h	2023-03-16 13:53:54.623920859 -0400
@@ -620,7 +620,7 @@ struct gfs_dinode {
 	char di_reserved[56];
 };
 
-struct gfs_sb {
+struct __attribute__ ((__may_alias__)) gfs_sb {
 	/*  Order is important; need to be able to read old superblocks
 	    in order to support on-disk version upgrades */
 	struct gfs2_meta_header sb_header;

Comment 5 Marek Polacek 2023-03-29 22:29:15 UTC
Reviewing this again, I think it's not a bug in the compiler, just a somewhat overzealous optimization.

Moving to gfs2-utils.

Comment 6 Andrew Price 2023-03-30 07:29:59 UTC
(In reply to Marek Polacek from comment #5)
> Reviewing this again, I think it's not a bug in the compiler, just a
> somewhat overzealous optimization.
> 
> Moving to gfs2-utils.

Overzealous optimization is not a bug in gfs2-utils. Let's close this WONTFIX if that's the intention.

Comment 7 Jakub Jelinek 2023-03-30 08:51:54 UTC
This boils down to
typedef unsigned char uint8_t;
typedef unsigned int uint32_t;
typedef unsigned long long uint64_t;
typedef long long int64_t;
typedef uint32_t __be32;
typedef uint64_t __be64;
typedef uint32_t __u32;
typedef uint8_t __u8;
struct gfs2_inum
{
  __be64 no_formal_ino;
  __be64 no_addr;
};
struct lgfs2_inum
{
  uint64_t in_formal_ino;
  uint64_t in_addr;
};
struct lgfs2_sbd
{
  uint32_t sd_bsize;
  uint32_t sd_fs_format;
  uint32_t sd_multihost_format;
  uint32_t sd_flags;
  union
  {
    struct lgfs2_inum sd_meta_dir;
    struct lgfs2_inum sd_jindex_di;
  };
  struct lgfs2_inum sd_root_dir;
  struct lgfs2_inum sd_rindex_di;
  struct lgfs2_inum sd_quota_di;
  struct lgfs2_inum sd_license_di;
  uint32_t sd_bsize_shift;
  uint32_t sd_seg_size;
  char sd_lockproto[64];
  char sd_locktable[64];
  uint8_t sd_uuid[16];
  uint32_t sd_fsb2bb;
  uint32_t sd_fsb2bb_shift;
  uint32_t sd_diptrs;
  uint32_t sd_inptrs;
  uint32_t sd_jbsize;
  uint32_t sd_hash_bsize;
  uint32_t sd_hash_bsize_shift;
  uint32_t sd_hash_ptrs;
  uint32_t sd_blocks_per_bitmap;
  uint32_t sd_max_height;
  uint32_t sd_max_jheight;
  uint64_t sd_heightsize[10];
  uint64_t sd_jheightsize[10];
  unsigned int jsize;
  unsigned int rgsize;
  unsigned int qcsize;
  int64_t sd_time;
  int device_fd;
  int path_fd;
  uint64_t fssize;
  uint64_t blks_total;
  uint64_t blks_alloced;
  uint64_t dinodes_alloced;
  unsigned int gfs1:1;
};
struct gfs2_meta_header
{
  __be32 mh_magic;
  __be32 mh_type;
  __be64 __pad0;
  __be32 mh_format;
  union
  {
    __be32 mh_jid;
    __be32 __pad1;
  };
};
struct gfs_sb
{
  struct gfs2_meta_header sb_header;
  __be32 sb_fs_format;
  __be32 sb_multihost_format;
  __be32 sb_flags;
  __be32 sb_bsize;
  __be32 sb_bsize_shift;
  __be32 sb_seg_size;
  struct gfs2_inum sb_jindex_di;
  struct gfs2_inum sb_rindex_di;
  struct gfs2_inum sb_root_di;
  uint8_t sb_lockproto[64];
  uint8_t sb_locktable[64];
  struct gfs2_inum sb_quota_di;
  struct gfs2_inum sb_license_di;
  char sb_reserved[96];
};
struct gfs2_sb
{
  struct gfs2_meta_header sb_header;
  __be32 sb_fs_format;
  __be32 sb_multihost_format;
  __u32 __pad0;
  __be32 sb_bsize;
  __be32 sb_bsize_shift;
  __u32 __pad1;
  struct gfs2_inum sb_master_dir;
  struct gfs2_inum __pad2;
  struct gfs2_inum sb_root_dir;
  char sb_lockproto[64];
  char sb_locktable[64];
  struct gfs2_inum __pad3;
  struct gfs2_inum __pad4;
  __u8 sb_uuid[16];
};
void _ck_assert_failed (const char *file, int line, const char *expr, const char *msg, ...) __attribute__((noreturn)) __attribute__((format (printf, 4, 5)));
void _mark_point (const char *file, int line);

void
lgfs2_inum_out (const struct lgfs2_inum *i, void *inp)
{
  struct gfs2_inum *in = inp;
  in->no_formal_ino = ((__be64) __builtin_bswap64 ((i->in_formal_ino)));
  in->no_addr = ((__be64) __builtin_bswap64 ((i->in_addr)));
}

void
lgfs2_sb_out (const struct lgfs2_sbd *sdp, void *buf)
{
  struct gfs2_sb *sb = buf;
  struct gfs_sb *sb1 = buf;
  sb->sb_header.mh_magic = ((__be32) __builtin_bswap32 ((0x01161970)));
  sb->sb_header.mh_type = ((__be32) __builtin_bswap32 ((1)));
  sb->sb_header.mh_format = ((__be32) __builtin_bswap32 ((100)));
  sb->sb_fs_format = ((__be32) __builtin_bswap32 ((sdp->sd_fs_format)));
  sb->sb_multihost_format = ((__be32) __builtin_bswap32 ((sdp->sd_multihost_format)));
  sb1->sb_flags = ((__be32) __builtin_bswap32 ((sdp->sd_flags)));
  sb->sb_bsize = ((__be32) __builtin_bswap32 ((sdp->sd_bsize)));
  sb->sb_bsize_shift = ((__be32) __builtin_bswap32 ((sdp->sd_bsize_shift)));
  sb1->sb_seg_size = ((__be32) __builtin_bswap32 ((sdp->sd_seg_size)));
  lgfs2_inum_out (&sdp->sd_meta_dir, &sb->sb_master_dir);
  lgfs2_inum_out (&sdp->sd_root_dir, &sb->sb_root_dir);
  __builtin_memcpy (sb->sb_lockproto, sdp->sd_lockproto, 64);
  __builtin_memcpy (sb->sb_locktable, sdp->sd_locktable, 64);
  lgfs2_inum_out (&sdp->sd_rindex_di, &sb1->sb_rindex_di);
  lgfs2_inum_out (&sdp->sd_quota_di, &sb1->sb_quota_di);
  lgfs2_inum_out (&sdp->sd_license_di, &sb1->sb_license_di);
  __builtin_memcpy (sb->sb_uuid, sdp->sd_uuid, 16);
}

void
check_sb1_out (void)
{
  char namechk[64];
  char buf[sizeof (struct gfs_sb)];
  struct lgfs2_sbd sbd;
  struct gfs_sb *sb;
  __builtin_memset (namechk, 0x5a, 64);
  __builtin_memset (buf, 0, sizeof (buf));
  __builtin_memset (&sbd, 0, sizeof (sbd));
  sbd.sd_fs_format = 0x5a5a5a51;
  sbd.sd_multihost_format = 0x5a5a5a52;
  sbd.sd_flags = 0x5a5a5a53;
  sbd.sd_bsize = 0x5a5a5a54;
  sbd.sd_bsize_shift = 0x5a5a5a55;
  sbd.sd_seg_size = 0x5a5a5a56;
  sbd.sd_jindex_di.in_formal_ino = 0x5a5a5a5a5a5a5a57;
  sbd.sd_jindex_di.in_addr = 0x5a5a5a5a5a5a5a58;
  sbd.sd_rindex_di.in_formal_ino = 0x5a5a5a5a5a5a5a59;
  sbd.sd_rindex_di.in_addr = 0x5a5a5a5a5a5a5a5a;
  sbd.sd_root_dir.in_formal_ino = 0x5a5a5a5a5a5a5a5b;
  sbd.sd_root_dir.in_addr = 0x5a5a5a5a5a5a5a5c;
  __builtin_memset (sbd.sd_lockproto, 0x5a, sizeof (sbd.sd_lockproto));
  __builtin_memset (sbd.sd_locktable, 0x5a, sizeof (sbd.sd_locktable));
  sbd.sd_quota_di.in_formal_ino = 0x5a5a5a5a5a5a5a5d;
  sbd.sd_quota_di.in_addr = 0x5a5a5a5a5a5a5a5e;
  sbd.sd_license_di.in_formal_ino = 0x5a5a5a5a5a5a5a5f;
  sbd.sd_license_di.in_addr = 0x5a5a5a5a5a5a5a50;
  lgfs2_sb_out (&sbd, buf);
  sb = (struct gfs_sb *) buf;
  (__builtin_bswap32 ((uint32_t) (__be32) (sb->sb_fs_format)) == 0x5a5a5a51) ? _mark_point ("ondisk1.c", 283) : _ck_assert_failed ("ondisk1.c", 283, "Assertion '" "__builtin_bswap32 (( uint32_t)(__be32)(sb->sb_fs_format)) == 0x5a5a5a51" "' failed", ((void *) 0));
}

when compiled with -O2 -fdump-tree-optimized=/dev/stdout
before https://gcc.gnu.org/r11-4586 it shows conditional _ck_assert_failed call at the end while starting with it up to latest trunk it is unconditional at -O2 unless -fno-ipa-modref.
Let me try to reduce it some more...

Comment 8 Jakub Jelinek 2023-03-30 09:27:20 UTC
I'd lean towards this being invalid C.
Somewhat more reduced:
typedef unsigned char uint8_t;
typedef unsigned int uint32_t;
typedef unsigned long long uint64_t;
typedef long long int64_t;
struct lgfs2_sbd
{
  uint32_t sd_fs_format;
  char sd_locktable[64];
  uint8_t sd_uuid[16];
  uint64_t fssize;
};
struct gfs2_meta_header
{
  uint32_t mh_magic;
  uint32_t mh_type;
  uint32_t mh_format;
  union
  {
    uint32_t mh_jid;
  };
};
struct gfs_sb
{
  struct gfs2_meta_header sb_header;
  uint32_t sb_fs_format;
  uint8_t sb_locktable[64];
  char sb_reserved[96];
};
struct gfs2_sb
{
  struct gfs2_meta_header sb_header;
  uint32_t sb_fs_format;
  char sb_locktable[64];
  uint8_t sb_uuid[16];
};

__attribute__((noinline)) void
lgfs2_sb_out (const struct lgfs2_sbd *sdp, void *buf)
{
  struct gfs2_sb *sb = buf;
  sb->sb_header.mh_magic = ((uint32_t) __builtin_bswap32 (0x01161970));
  sb->sb_header.mh_type = ((uint32_t) __builtin_bswap32 (1));
  sb->sb_header.mh_format = ((uint32_t) __builtin_bswap32 (100));
  sb->sb_fs_format = ((uint32_t) __builtin_bswap32 (sdp->sd_fs_format));
  __builtin_memcpy (sb->sb_locktable, sdp->sd_locktable, 64);
  __builtin_memcpy (sb->sb_uuid, sdp->sd_uuid, 16);
}

void
check_sb1_out (void)
{
  char buf[sizeof (struct gfs_sb)];
  struct lgfs2_sbd sbd;
  struct gfs_sb *sb;
  __builtin_memset (buf, 0, sizeof (buf));
  __builtin_memset (&sbd, 0, sizeof (sbd));
  sbd.sd_fs_format = 0x5a5a5a51;
  __builtin_memset (sbd.sd_locktable, 0x5a, sizeof (sbd.sd_locktable));
  lgfs2_sb_out (&sbd, buf);
  sb = (struct gfs_sb *) buf;
  if (__builtin_bswap32 (sb->sb_fs_format) != 0x5a5a5a51)
    __builtin_abort ();
}
The thing is that lgfs2_sb_out writes into the buf using struct gfs2_sb dynamic type, but the caller reads from it using struct gfs_sb type.
Pedantically, even the char buf[sizeof (struct gfs_sb)]; declaration gives the buffer char buf[whatever] as the effective type and so accessing
it through different types is UB (unlike if buf was say heap allocated, then it has no declared type and so stores through some type
give it effective type).  See e.g. C17 6.5/6:
"The effective type of an object for an access to its stored value is the declared type of the object, if any."
plus long description of what happens if it doesn't have any (say malloc allocated etc.)
and 6.5/7:
"An object shall have its stored value accessed only by an lvalue expression that has one of the
following types:
— a type compatible with the effective type of the object,
— a qualified version of a type compatible with the effective type of the object,
— a type that is the signed or unsigned type corresponding to the effective type of the object,
— a type that is the signed or unsigned type corresponding to a qualified version of the effective
type of the object,
— an aggregate or union type that includes one of the aforementioned types among its members
(including, recursively, a member of a subaggregate or contained union), or
— a character type."
Now, GCC generally allows that because of C++ placement new where declaring a buffer unsigned char or std::byte array
and doing a placement new into it changes the effective type.
But, assuming this was C++ and you'd do a placement new into it, still, lgfs2_sb_out stores using the gfs2_sb type
and caller reads it using gfs_sb.  And gfs_sb and gfs2_sb structures are really incompatible from the alising POV.
A fix might be to use a union of struct gfs_sb and struct gfs2_sb and access it through in the access path visible
union of those both in the callee and caller.

Comment 9 Andrew Price 2023-04-03 17:57:33 UTC
I'm happy to leave this bug closed and address the issue in gfs2-utils, but I've narrowed down a small test case to convince myself of the problem, which might be of interest:

$ cat test.c
#include <stdio.h>

struct a { unsigned n; };
struct b { unsigned n; };

__attribute__((noinline))
void out(unsigned *n, void *buf)
{
	((struct b *)buf)->n = __builtin_bswap32(*n);
}

int main(void)
{
	char buf[sizeof(struct a)] = {0};
	struct a *a = (struct a *)buf;
	unsigned n = 0x5a5a5a51;

	out(&n, buf);
	printf("0x%x\n", __builtin_bswap32(a->n));
	return 0;
}

$ gcc -O0 -Wall -Wextra test.c -o test; ./test
0x5a5a5a51
$ gcc -O2 -Wall -Wextra test.c -o test; ./test
0x0

Changing the cast in out() from (struct b *) to (struct a *) fixes the bug, which tracks with the UB explanation.

Compiling with clang, it outputs 0x5a5a5a51 at all optimization levels but presumably that just means it doesn't make use of this UB for optimization in the same way.

Comment 10 Jakub Jelinek 2023-04-03 18:10:40 UTC
This is still UB for the same two reasons as described earlier.
One is that the buf variable has char[4] type and so the effective type is always just that, this is C, so you can't do a placement new into the buffer to change the effective type.
And the second reason is that the store happens through one type (struct b) while read through another one (struct a) from aliasing POV incompatible with it.
Don't do that.  Not to mention the buffer isn't guaranteed to be properly aligned for the type either.


Note You need to log in before you can comment on or make changes to this bug.