给SnapRAID替换硬盘

之前在我的乞丐版NAS及配置这篇文章中介绍了NAS的配置。
正常工作近5年之后,其中一块硬盘开始报了SMART Error。

SMART Error

一开始的SMART Error是这样:

# SMART error (CurrentPendingSector) detected on host
Device: /dev/sde [SAT], 1 Currently unreadable (pending) sectors

稳定了几天之后,pending sectors变为了这样:

Device: /dev/sde [SAT], 41 Currently unreadable (pending) sectors

看到unreadable sector明显增加了。

看一下snapraid smart的输出

$ sudo snapraid smart
SnapRAID SMART report:

   Temp  Power   Error   FP Size
      C OnDays   Count        TB  Serial           Device    Disk
 -----------------------------------------------------------------------
     32   1750       0   5%  6.0  WD-WX31D88R41HT  /dev/sdb  d1
     35   1749 selferr 100%  6.0  WD-WX31D88AEC9Z  /dev/sde  d2
     33   1749       0   4%  6.0  WD-WX31D88AERKL  /dev/sdd  parity

d2这块盘的寿命快到了。

替换硬盘

在SnapRAID中替换硬盘的步骤不复杂,记录一下。

  1. 停止所有会写数据的服务,比如把日常的docker容器都停了

    docker stop $(docker ps -a -q)
  2. 确保数据正确,如果有,sync一下

    $ sudo snapraid diff # 看下有没有数据不一致
    Loading state from /var/snapraid/snapraid.content...
    Comparing...
    ...
    6 removed
    18 updated
    There are differences!
    
    # 有不一致,跑一下sync
    $ sudo snapraid sync
    ...
    Everything OK
    ...
    Verifying...
    Verified /mnt/data/disk2/snapraid.content in 0 seconds
    Verified /var/snapraid/snapraid.content in 0 seconds
    Verified /mnt/data/disk1/snapraid.content in 0 seconds
    
    # 再次确保数据一致
    $ sudo snapraid diff
    Loading state from /var/snapraid/snapraid.content...
    Comparing...
    ...
    No differences
  3. 给新硬盘创建分区,并格式化。使用之前文章中相同的方式。

    $ sudo parted -a optimal /dev/sdc
    ...
    $ sudo mkfs.ext4 -m 2 -T largefile4 /dev/sdc1
  4. 从旧硬盘拷数据。这一步花了好几个小时…

    # 新硬盘临时mount在 /mnt/tmp
    sudo mount -t auto /dev/sdc1 /mnt/tmp
    # 拷贝旧硬盘数据
    cp -av /mnt/data/disk2/. /mnt/tmp
  5. 重新mount文件系统

    # umount mergerfs
    $ sudo umount /mnt/storage
    
    # umount 旧硬盘
    $ sudo umount /mnt/data/disk2
    
    # 修改/etc/fstab,使用新硬盘,mount到原始位置
    /dev/disk/by-id/ata-XXXXXXXXXXX-part1 /mnt/data/disk2 ext4 defaults 0 2
    
    # 重新mount
    sudo mount /mnt/data/disk2/
  6. 使用snapraid确认数据,可以注意到snapraid发现d2这块硬盘的uuid变了。
    snapraid check这一步也要花好几个小时(胆大的也可以跳过)

    $ sudo snapraid diff
    Loading state from /var/snapraid/snapraid.content...
    UUID change for disk 'd2' from 'dd5c3760-1e4e-4d72-b710-56c782f416c3' to '55a67bea-3ca1-4cbe-b9ed-83ca9825d627'
    Comparing...
    WARNING! UUID is changed for disks: 'd2'. Not using inodes to detect move operations.
    No differences
    
    # 检查数据一致性
    $ sudo snapraid check -a -d d2
    Self test...
    Loading state from /var/snapraid/snapraid.content...
    UUID change for disk 'd2' from 'dd5c3760-1e4e-4d72-b710-56c782f416c3' to '55a67bea-3ca1-4cbe-b9ed-83ca9825d627'
    Selecting...
    Using 1235 MiB of memory for the file-system.
    Initializing...
    Selecting...
    Hashing...
    8%, 191365 MB, 123 MB/s, 524 stripe/s, CPU 0%, 4:22 ETA TA
  7. 最后一步,sync一下

    $ sudo snapraid sync

到这里,snapraid相关的事情就完成了。

其它

相比之前文章里的aufs,后来在ubuntu不再支持aufs的情况下,改用mergerfs了,性能也还行。

我的配置如下

/mnt/data/* /mnt/storage fuse.mergerfs defaults,allow_other,use_ino,cache.files=partial,dropcacheonclose=true,category.create=mfs,minfreespace=20G,fsname=mergerfs 0 0

最后把旧硬盘拔掉,重新mount /mnt/storage目录,全部工作搞定。

Share

关于隔离的两三事

被隔离了,有大把的时间,记录一下,这神奇的社会,这荒诞的政策。

时间线

  • 2022.08.30
    上班路上紧急通知,不要去办公楼,可能有疫情,于是开始居家办公。
  • 2022.08.31
    正常居家办公。有一例新增,是隔壁10号楼的同学。
  • 2022.09.01白天
    继续居家办公。群里开始有人说接到疾控电话说是密接,要集中隔离。
    一开始有少量的同学申诉,通过发工位证明表示不是10号楼的员工,从密接转为高筛。这时疾控的反应还算正常。
  • 2022.09.01傍晚
    接到北蔡卫生院的电话,说接到浦东疾控通知,闵行疾控认为我是密接,要集中隔离。
    尝试用同样的方式申诉,无效。打了若干电话,浦东疾控表示,密接的名单需要由闵行疾控撤销,而名单是公司给的(?!)
    因为还算在申诉中,暂时没有集中隔离。
  • 2022.09.02
    继续各种电话申诉,包括闵行疾控,12345,等等。反应是:
    * 闵行疾控:我们不管,请打12345。
    * 12345:好的,我们会反映,之后会联系你。(结果至今没有任何回复)
    * 浦东疾控、北蔡卫生院:密接的名单需要由闵行疾控撤销(约等于复读机)。再不撤销就要上门来转运了。
    然而之后只有人上门做了核酸,并没有接到电话要求转运,继续呆家里。
    居委接到了通知,上门装了门磁。
  • 2022.09.03
    核酸码变红了,于是继续申诉:
    * 随申办:提交了申诉的材料,只有一个app的提示,至今没有回复。
    * 12345:好的,我们会反映,1个工作日之后会联系你,至今没有回复。
    由于变红码了,小学老师也收到了通知,发消息表示小朋友不能上学了,表示理解。
    和家人商量之后,决定,如果再次来电话要求集中隔离,就配合,以免影响之后的解除。
    等到晚上,终于接到了浦东疾控的电话,说名单还没有撤销,要求转运。
    说好一个小时之后来车子,等了两个多小时还没来,打了电话之后才说马上安排。(这么看如果不打电话,是不是就不来了?)
    晚上10点多车子才到,开到了离家不远的集中隔离酒店。
  • 2022.09.04
    酒店隔离中。继续申诉:
    * 12345:要求家里门磁的时间从29号开始算。没有回应。
    公司群里有的人解除了密接,有的人要隔离7天,有的人要隔离10天,但时间都是按进酒店开始算的。按国家规定是从阳性确诊那天开始算的。于是找了国家卫健委的网站,去留言申诉上海疾控随意延长隔离时间。(目前也没有任何回复)
    这期间,有的同事红码变成了绿码,但他家娃的码又变红了,不知道什么逻辑。

吐槽

  1. 阳性确诊是在8月29号,在10号楼发现的。然而有大量非10号楼的员工,以前29号当天没有去过10号楼的员工都在密接名单里,这数据精准程度可见一般。
  2. 由于有大量同学申诉(有成功的,也有失败的),开始在名单上扯皮。疾控说名单是公司提供的,不精准。然而我们都知道在楼栋入口处都有所谓的“数字哨兵”,每一个人、每一次进楼都是要扫码的,这个数据显然政府(或者疾控)是有的,为什么还要公司上报名单?
  3. 关于做核酸,9月3号当天是浦东全员核酸,然而我们家装了门磁理论上不应该出门。等了一天没有人上门。还需要打电话给居委,居委上报之后,才安排了人做核酸。
  4. 今天(9月4号)听家人说,还要给小弟(8个月的baby)做核酸,被骂回去了。真把命令当回事了。。。

疫情到现在2年多,快3年了,政策居然几乎没变。这盛世奇景什么时候会结束呢?

Share

单口旁路由,以及配置不同的设备用不同的路由

最近在折腾旁路由,然后出现了一个需求:对于不同的设备,要指定不同的路由。
比如说

  • 绝大部分普通设备(比如各种IoT设备)走原来的路由,全部在墙内;
  • 指定几个设备(比如说手机、iPad等)走旁路由,由旁路由来分流墙内外的流量。

发现用dnsmasq的话,这个问题很好解决,于是顺便写个文章记录一下。

旁路由

家里的网络很简单,电信光猫改桥接,路由器接光猫,所有设备都在一个LAN里。
之前看一些测试,路由器翻墙的话,性能有瓶颈,而当NAS用的蜗牛星际平时很闲,正好用来做旁路由。
目标是,在KVM虚拟机里面运行Lede,只开科学上网工具。

配置KVM

我的蜗牛星际是单口千兆的版本,所以必须配置bridge来给KVM虚拟机用。
Ubuntu用netplan来配置很简单:

$ cat /etc/netplan/01-netcfg.yaml
network:
  version: 2
  renderer: networkd
  ethernets:
    enp3s0:
      dhcp4: no
      dhcp6: no
  bridges:
    br0:
      interfaces: [enp3s0]
      dhcp4: no
      dhcp6: no
      addresses: [<lan-ip>/24]
      gateway4: <lan-gateway>
      nameservers:
        addresses: [<nameservers>]
      parameters:
        stp: true

然后 netplan apply就好了

安装好KVM之后,运行virt-manager用图形界面创建虚拟机(这样方便一点),加载lede的image,只要注意网络那边手动写之前创建好的br0Device modevirtio(性能最好)就好了。

之后配置这个虚拟机开机自动启动:

virsh autostart lede # lede是建VM时起的名字

配置lede

初次使用时,lede默认的IP是192.168.1.1,可以在console里先把它的IP改成自己想要的。

virsh console lede
## 之后就是在lede的VM里操作
# cat /etc/config/network
...
config interface 'lan'
        option ifname 'eth0'
        option proto 'static'
        option ipaddr '<ip>'   # 设置Lede的LAN IP
        option netmask '255.255.255.0'
        option gateway '<gateway>'
        option ip6assign '60'
        option multipath 'off'
        option dns '<dns, or gateway>'
...

# /etc/init.d/network restart #重启网络服务

然后可以在浏览器里打开lede,在网络->接口的配置里

  • 把LAN口的DHCP关掉
  • 在WAN口的物理设置里,取消“桥接接品”,接口选eth0(因为我只有一个网口)
  • 在WAN口的基本设置里,选”DHCP客户端”

现在LEDE就可以愉快地上网啦。

配置v2ray

装旁路由的目的就是为了科学上网,我一般用v2ray。
这个东东本来应该在Lede的软件中心里出现的,不过由于”你懂的“原因,它下架了,只能手动下载离线包,离线安装。

  • 自行Google “离线 v2ray_2.3.7.tar.gz”(后果自负),比如说,https://github.com/hq450/fancyss_history_package/tree/master/fancyss_X64,安装好。
  • 在服务器列表里,导入 vmess::// 头的链接
  • 在帐号设置里,选一个服务器,根据自己需要选“大陆白名单”或者gfwlist,打开”代理开关“,看到国外链接正常的时候,说明没问题了。

配置不同设备用不同的路由

这个时候如果手动配置一个设备的gateway到Lede的LAN IP,就能正常科学上网了。
不过我希望由DHCP server来给指定的设备(比如说根据mac地址)来分配不同的gateway。
Google之后发现,dnsmasq正好可以用来做这事儿,而我主路由的梅林固件正好就是用dnsmasq的,并且还用它来支持电信IPTV。
原理是,通过对mac地址打tag,然后根据tag来设置对应的gateway和nameserver (参考 http://www.thekelleys.org.uk/dnsmasq/docs/dnsmasq-man.html)

# cat /jffs/configs/dnsmasq.d/customgateway.conf
dhcp-mac=red,<mac1>   # 把要使用旁路由的设备的mac地址写在这里,用相同的tag名字
dhcp-mac=red,<mac2>
dhcp-option = net:red, option:router, <gateway-IP>   # 指定旁路由的IP作为gateway
dhcp-option = net:red, 6, <nameserver-IP>   # 指定旁路由的IP作为nameserver(也可以用自定义的nameserver,看需求)

然后重启dnsmasq服务

# service restart_dnsmasq

需要注意的是,有些手机可能会用随机的MAC地址,在手机上设置一下,连家里的wifi,就用真正的mac地址吧。
手机、iPad连上wifi之后,看一下gateway,如果已经是旁路由的地址了,说明配置没问题,可以自由地上网了。
而其它设备不受影响,还是走原来的路由。

完工。

Share

异地NAS备份

之前的文章介绍了我的乞丐版NAS及配置,是放在家里的单个NAS。虽然做了SnapRAID,坏一块盘也没关系,然而假如由于某种原因这个NAS里的盘全坏了,数据就没了。
数据备份有个3-2-1原则:

  • 3份数据
  • 2种存储介质
  • 1个异地备份

过年回老家之后,由于新冠病毒(2019-nCOV)一直呆在老家,正好可以折腾一下异地的NAS备份,这样我也有自己的2-1-1的备份了:

  • 2份数据
  • 1种存储介质
  • 1个异地备份

对于家用来说,已经足够安全了。
这时记录一下我的异地NAS备份的配置。

硬件

  • 跟之前一样,还是蜗牛星际A款单口千兆,350包邮(2020.02价格,比之前涨了100!)
  • 西数紫盘 4T*1 (目前的容量,一块就够了,以后有需要就再添加)

软件

对于放在老家的NAS,出于成本考虑,就不做RAID了;而目前只用一块硬盘,为了方便以后扩展,LVS是最方便的。
既然是异地备份,一开始想的是用rsync就好了。
不过后来想想,当人在老家的时候,可以直接备份数据在老家的NAS上;并且老爸老妈他们手机里的数据也可以直接备份在本地,所以我需要的是一个双向的同步工具。用两条rsync命令当然也可以,不过应该会有更合适的工具。

综合上面的需要,最后决定用的是:

  • Ubuntu (目前装的是19.10)
  • LVS方便未来扩展空间
  • wireguard用来连接两台NAS
  • unison用来双向同步
  • 其它工具和原来的NAS相同(比如hdparm, smartmontools等)

LVS

以前不太用LVS,觉得麻烦,后来在公司的某台服务器上配置了LVS之后,觉得用起来很爽,现在在这台异地NAS上正好可以用起来。
把这块4T的硬盘分成两个LV(Logical Volume),一个用来放不用长期保存的(如下载的视频之类)文件,另一个用来作为我的NAS的异地备份,都用ext4文件系统。

# 假设用fdisk或者parted工具创建了一个/dev/sdb1,包含/dev/sdb的整个空间
pvcreate /dev/sdb1   # 创建physical volume
vgcreate nas_data_vg /dev/sdb1   # 创建名为nas_data_vg的virtual group
lvcreate -n nas_download -L 800G nas_data_vg   # 创建一个800G大小的LV
lvcreate -n nas_data nas_data_vg -l 100%FREE   # 创建一个包含剩余所有空间的LV

# 格式化
mkfs.ext4 /dev/nas_data_vg/nas_download
mkfs.ext4 /dev/nas_data_vg/nas_data

# 查看blkid
blkid /dev/nas_data_vg/nas_data
blkid /dev/nas_data_vg/nas_download

# 编辑/etc/fstab自动挂载
# cat /etc/fstab
UUID=<blk-id-of-nas_data> /mnt/storage ext4 defaults 0 0
UUID=<blk-id-of-nas_download> /mnt/downloads ext4 defaults 0 0

Wireguard

之前写过一篇文章讲怎么配置Wireguard server和怎么在手机上用Wireguard,在Ubuntu里配置一个client也是超级简单的事情。然后用systemd让它开机自动启动就好了,这样这个异地的NAS开机完,就自动连着我原来的NAS。

sudo apt install wireguard

# cat /etc/wireguard/wg0.conf
[Interface]
Address = <Self IP in the VPN>/24
PrivateKey = <private-key>
[Peer]
PublicKey = <public-key-on-wireguard-server>
# AllowedIPs = 0.0.0.0/0 # 如果想让NAS的默认网关走远程的网络
AllowedIPs = 192.168.2.0/24 # 如果只想在WireGuard的网段互相访问
Endpoint = <wireguard-server>:<port>

sudo systemctl enable [email protected]   # Enable这个service
systemctl start [email protected]   #开始这个service

unison

搜索了一下双向同步的工具,有一些开源的,根据https://askubuntu.com/questions/727304/automatically-do-a-two-way-sync-of-two-directories 这里面的推荐,我最后选择了用unison,看上去比较清晰简单。
安装很简单,Ubuntu官方源里有就有,直接apt就装好了。

sudo apt install unison

unison的基础用法很简单,unison root1 root2就把root1和root2同步了,先按官网的教程里,把玩一下a.tmpb.tmp,然后就可以写自己的脚本了。

$ cat ~/bin/sync_between_nas.sh
#!/bin/bash

if [ $# -ne 1 ]; then
  echo "Usage:"
  echo "$0 <dir>"
  echo ""
  echo "Two-way sync <dir> between my NAS in /mnt/storage"
  exit 1 
fi

dir_to_sync=$1
src=ssh://$user@$nas//mnt/storage/$1
dst=/mnt/storage/$1

if ssh $user@$nas "[ ! -d $dst ]"; then
  echo $dst does not exist!
  exit 1
fi

echo To sync from \"$src\" to \"$dst\"...
sudo unison $src $dst -batch -owner -group -prefer newer -times -nodeletion $src -nodeletion $dst

这个脚本会同步两个NAS的/mnt/storage/目录,主要功能在最后一行:

  • -batch: 表示不需要用户的输入(否则它会问用户使用哪种action)
  • -owner, -group: 保留原有的owner:group。注意:
  • 这里两个NAS都需要创建同样的owner、group
  • 因为文件传输之后需要chown,所以这个脚本要以su用户来执行(不知道有没有更好的方式)
  • -prefer newer:如果有冲突,总是以更新的文件为准
  • -times:同步文件的修改时间
  • -nodeletion $src -nodeletion $dst: 对$src和$dst都不进行删除,以防有文件被误删,并被同步到另一个NAS

然后,有一个辅助脚本来指定要备份的目录,并调用上面这个脚本。

#!/bin/bash

set -e
dirs_to_sync="<dir1> <dir2> ..."
for d in $dirs_to_sync; do
  echo $d
  /home/mine/bin/sync_between_nas.sh $d
done

最后,配置一个cron job,就可以愉快地每天同步数据啦。记得让cron发邮件,以便检查同步的状态。

$ sudo crontab -l
MAILTO="<my-email-address>"
10 2 * * * /usr/bin/flock /tmp/sync_my_nas.lock /path/to/sync_my_nas_dirs.sh

后记

后来有朋友推荐了Syncthing,看上去有点像BTSync之类的,以后也许可以试试。

Share

From a CI bug to systemd, to GCC

I was tracing a bug in sdbusplus found by OpenBMC CI, it leads me into the code in systemd, and eventually get me into GCC.
To get a short introduction, refer to https://lists.ozlabs.org/pipermail/openbmc/2019-December/019884.html
Here is the full story of the investigation.

The sdbusplus CI issue

A CI issue is found in sdbusplus, that the valgrind reports the below error.

==5290== Syscall param epoll_ctl(event) points to uninitialised byte(s)
==5290== at 0x4F2FB08: epoll_ctl (syscall-template.S:79)
==5290== by 0x493A8F7: UnknownInlinedFun (sd-event.c:961)
==5290== by 0x493A8F7: sd_event_add_time (sd-event.c:1019)
==5290== by 0x190BB3: phosphor::Timer::Timer(sd_event*, std::function) (timer.hpp:62)
==5290== by 0x192B93: TimerTest::TimerTest() (timer.cpp:25)
==5290== by 0x193A13: TimerTest_timerExpiresAfter2seconds_Test::TimerTest_timerExpiresAfter2seconds_Test() (timer.cpp:85)
...
==5290== by 0x4A90917: main (gmock_main.cc:69)
==5290== Address 0x1fff00eafc is on thread 1's stack
==5290== in frame #0, created by epoll_ctl (syscall-template.S:78)
==5290==

Clearly, valgrind detects that some uninitialized data is used.
However, this issue is not 100% reproduced, it only occurs sometimes, how could that be?

At a first glance, it’s sdbusplus‘s Timer class that invokes sd_event_add_time() from libsystemd, and eventually invokes epoll_ctl().
So I would suspect something may be wrong in Timer that it may pass uninitialized data to sd_event_add_time().

Investigation in sdbusplus

Let’s see the related code.

void initialize()
{
    ...
    auto r = sd_event_add_time(
        event, &eventSource,
        CLOCK_MONOTONIC, // Time base
        UINT64_MAX,      // Expire time - way long time
        0,               // Use default event accuracy
        [](sd_event_source* eventSource, uint64_t usec, void* userData) {
            auto timer = static_cast<Timer*>(userData);
            return timer->timeoutHandler();
        },     // Callback handler on timeout
        this); // User data
    ...
}

The event is a pointer that is already initialized;
The eventSource is the out-parameter.
Others are just simple data or a lambda, nothing suspicious.

Investigation in libsystemd

So let’s dive into libsystemd to see what exactly happens.
The related code is in sd_event_add_time().

_public_ int sd_event_add_time(sd_event *e, ...) {
    ...
    if (d->fd < 0) {
        r = event_setup_timer_fd(e, d, clock);
        if (r < 0)
            return r;
    }
    ...
}

Where:

  • e is sd_event*, and clock is clockid_t, both are passed into this function
  • d is struct clock_data* initialized in this function So nothing is wrong.

Let’s see event_setup_timer_fd() then.

static int event_setup_timer_fd(...) {
    struct epoll_event ev;
    ...
    ev = (struct epoll_event) {
        .events = EPOLLIN,
        .data.ptr = d,
    };
    r = epoll_ctl(e->epoll_fd, EPOLL_CTL_ADD, fd, &ev);
    ...
}

The epoll_fd, fd, and ev, are all initialized, are they?

Let’s see how epoll_ctl is implemented in kernel source.

SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd,
        struct epoll_event __user *, event)
{
    ...
    if (ep_op_has_event(op) &&
        copy_from_user(&epds, event, sizeof(struct epoll_event)))
        goto error_return;
    ...
}

Be noted that valgrind says Syscall param epoll_ctl(event) points to uninitialised byte(s), and here we do see that it’s copying the whole event struct from userspace, which means the event contains uninitialized bytes.
Let’s go back to see how the event struct is initialized:

    ev = (struct epoll_event) {
        .events = EPOLLIN,
        .data.ptr = d,
    };

And let’s see how the struct is defined in glibc.

typedef union epoll_data
{
  void *ptr;
  int fd;
  uint32_t u32;
  uint64_t u64;
} epoll_data_t;

struct epoll_event
{
  uint32_t events;  /* Epoll events */
  epoll_data_t data;    /* User data variable */
} __EPOLL_PACKED;

Hmm, the events is a uint32_t and is initialized, and the data is initialized as well, it looks fine…
Is it really fine?
data is a union and should be at least 64 bits, while events is uint32_t, which is 32 bits, there could be padding inside the epoll_event padding if it’s not packed.
Hey, there is __EPOLL_PACKED… let’s grep it in glibc:

$ gg __EPOLL_PACKED
ChangeLog.old/ChangeLog.18:     (__EPOLL_PACKED): Define to empty if not defined by
ChangeLog.old/ChangeLog.18:     (struct epoll_event): Use __EPOLL_PACKED to make possibly packed.
sysdeps/unix/sysv/linux/sys/epoll.h:#ifndef __EPOLL_PACKED
sysdeps/unix/sysv/linux/sys/epoll.h:# define __EPOLL_PACKED
sysdeps/unix/sysv/linux/sys/epoll.h:} __EPOLL_PACKED;
sysdeps/unix/sysv/linux/x86/bits/epoll.h:#define __EPOLL_PACKED __attribute__ ((__packed__))

It is defined to __attribute__ ((__packed__)) for x86, and not defined for other architectures.
Remember that the issue is not 100% reproduced, right?
The OpenBMC CI backend has both x86-64 and ppc64le servers, so we could guess that the padding causes the valgrind error, and it only happens on ppc64le but not on x86-64, because on x86-64 there is no padding at all.
From the CI log, it confirms that the guess is correct: the issue only occurs on the ppc64le CI server!

So let’s go back to the question code:

    ev = (struct epoll_event) {
        .events = EPOLLIN,
        .data.ptr = d,
    };

It’s using GCC’s Designated Initializers extension
I tried to google how GCC initialize the padding, there are discussions in StackOverlows and blogs, e.g. according to https://stackoverflow.com/questions/37642026/does-c-initialize-structure-padding-to-zero, it looks like the case:

padding for the remaining objects are guaranteed to be 0, but not for the members which has received the initializers.

But I do not see an official GCC doc talking about this.
Let’s do some experiments.

Testing in GCC

Giving below demo code to test how the padding is initialized:

#include <string.h>
#include <stdint.h>
#include <stdio.h>

struct struct_with_padding {
        uint32_t a;
        uint64_t b;
        uint32_t c;
};
int main()
{
        struct struct_with_padding s;
        memset(&s, 0xff, sizeof(s));
        s = (struct struct_with_padding) {
                .a = 0xaaaaaaaa,
                .b = 0xbbbbbbbbbbbbbbbb,
#ifdef SHOW_GCC_BUG
                .c = 0xdddddddd,
#endif
        };
        uint8_t* p8 = (uint8_t*)(&s);
        printf("data: ");
        for (size_t i = 0; i < sizeof(s); ++i)
        {
                printf("0x%02x ", p8[i]);
        }
        printf("\n");
        return 0;
}

Compile with or without SHOW_GCC_BUG, the result is different:

$ gcc -o test_padding test_padding.c
$ ./test_padding
data: 0xaa 0xaa 0xaa 0xaa 0x00 0x00 0x00 0x00 0xbb 0xbb 0xbb 0xbb 0xbb 0xbb 0xbb 0xbb 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

$ gcc -DSHOW_GCC_BUG -o test_padding test_padding.c
$ ./test_padding
data: 0xaa 0xaa 0xaa 0xaa 0xff 0xff 0xff 0xff 0xbb 0xbb 0xbb 0xbb 0xbb 0xbb 0xbb 0xbb 0xdd 0xdd 0xdd 0xdd 0xff 0xff 0xff 0xff

GCC behaves like:

  • If a struct is partial initialized, the all the padding are initialized to zero;
  • If a struct is fully initialized, the padding remains the old data. This is exactly what happens in my case!

How about clang?

$ clang -o test_padding test_padding.c
$ ./test_padding
data: 0xaa 0xaa 0xaa 0xaa 0x00 0x00 0x00 0x00 0xbb 0xbb 0xbb 0xbb 0xbb 0xbb 0xbb 0xbb 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

$ clang -DSHOW_GCC_BUG -o test_padding test_padding.c
$ ./test_padding
data: 0xaa 0xaa 0xaa 0xaa 0x00 0x00 0x00 0x00 0xbb 0xbb 0xbb 0xbb 0xbb 0xbb 0xbb 0xbb 0xdd 0xdd 0xdd 0xdd 0x00 0x00 0x00 0x00

OK, clang initializes the padding in both cases, good!

While I tried to file a bug to GCC, I found an exact same bug https://gcc.gnu.org/bugzilla/show_bug.cgi?id=77992, which is reported in 2016, and it looks like GCC is not going to fix it…

Follow-ups

  1. I tried to send a PR to systemd as a workaround that manually zero initialize the struct epoll_event.
    It’s under discussion, but likely the maintainer of systemd (@poettering) will not accept the PR, because it’s not really a systemd bug, instead, @poettering treats it a Valgrind bug (if not a GCC bug).
  2. Although I do not think it’s a Valgrind bug, a bug is filed to https://bugs.kde.org/show_bug.cgi?id=415621, there is no further feedback yet.
  3. Without GCC or systemd or valgrind’s fix, I have to add a valgrind suppression to OpenBMC CI https://gerrit.openbmc-project.xyz/c/openbmc/sdbusplus/+/25548, problem solved.

Summary

  • GCC has a bug of not initializing the padding.
  • systemd hits the bug on initializing struct epoll_event on non-x86 systems.
  • OpenBMC CI has both x86-64 and ppc64le systems. If a CI is run on ppc64le, the issue occurs.
  • Adding a valgrind suppression file fixes (or workarounds) the issue.
Share