Skip to main content

Cyber AI România

Vineri, 23 octombrie 2026

fserror și fsnotify în Linux: urmărim erorile I/O până în userspace

Un volum continuă să fie montat, dar o citire directă întoarce EIO. În loc să căutăm periodic expresii fragile în mesajele kernelului, vom primi evenimentul prin interfața fanotify, îl vom decoda într-un proces userspace și îl vom păstra într-un journal separat.

Laboratorul folosește un sistem de fișiere ext4 creat într-o imagine locală de 512 MiB. Device Mapper introduce temporar o eroare de citire numai pe acest dispozitiv de test. Niciun disc real nu este corupt, iar tabela liniară este restaurată automat chiar dacă testul eșuează.

La final rulează trei componente legate între ele:

  • fserror-monitor, un program C care cere FAN_FS_ERROR și decodează înregistrările ERROR și FID;
  • fserror-monitor.service, pornit sub un utilizator dedicat, cu singura capabilitate necesară pentru marca fanotify la nivel de filesystem;
  • fserror-health.timer, care verifică periodic dacă monitorul a rămas activ.

Evenimentele se citesc cu journalctl --namespace=fserror. Rezultatul verificabil este un mesaj cu eroarea brută, numărul errno, contorul de evenimente și identificatorul obiectului furnizat de kernel.

fserror transportă problema, iar fanotify stabilește contractul cu procesul

fserror nu este un program pe care îl instalăm și nici un daemon care analizează dmesg. Este infrastructură internă a kernelului, construită în jurul structurii super_block, prin care codul VFS și sistemele de fișiere pot raporta erori de metadata sau de I/O către fsnotify.

În Linux 7.0, această cale a fost standardizată pentru raportarea generică a erorilor de filesystem și de fișier. Linux 7.2, folosit aici, include infrastructura și suportul relevant pentru ext4. Acoperirea depinde totuși de sistemul de fișiere și de locul din kernel care raportează eroarea: FAN_FS_ERROR nu promite că orice eroare produsă de orice driver va deveni automat eveniment fanotify.

Fluxul laboratorului este:

dd cu O_DIRECT
      |
      v
Device Mapper: target "error"
      |
      v
operația de citire primește -EIO
      |
      v
fserror raportează eroarea pentru super_block/inode
      |
      v
fsnotify construiește FAN_FS_ERROR
      |
      v
fanotify queue
      |
      v
fserror-monitor citește ERROR + FID
      |
      v
journal namespace "fserror"

O notificare nu înlocuiește rezultatul operației. Aplicația care a făcut citirea trebuie să verifice în continuare codul său de retur. Aceeași separare contează și în aplicațiile descrise în ghidul despre io_uring și monitorizarea aplicațiilor I/O-intensive: completarea unei operații spune ce s-a întâmplat acelei cereri, în timp ce FAN_FS_ERROR informează un monitor despre o problemă observată în filesystem.

Evenimentul fanotify conține o structură fanotify_event_metadata, urmată de înregistrări cu lungime variabilă. Pentru cazul nostru sunt relevante:

  • FAN_EVENT_INFO_TYPE_ERROR, cu eroarea și error_count;
  • FAN_EVENT_INFO_TYPE_FID, cu fsid și un file handle;
  • FAN_Q_OVERFLOW, dacă procesul nu a golit coada suficient de repede.

error_count poate arăta că mai multe erori echivalente au fost reunite. Nu este un număr de sectoare defecte și nu trebuie interpretat ca rezultat al unei verificări de integritate.

Fixăm laboratorul la Fedora 44, kernel 7.2 și un volum ext4 izolat

Configurația de referință este Fedora Server 44 x86-64, cu kernelul Fedora 7.2.11-200.fc44, systemd 259 și minimum 2 GiB RAM. Sunt necesari aproximativ 700 MiB liberi: 512 MiB pentru imagine, plus journal și fișierele proiectului. Compilarea și testarea durează aproximativ 30–45 de minute.

Laboratorul trebuie rulat într-o mașină virtuală sau pe un sistem de test. Scriptul verifică numele Device Mapper înainte de orice modificare, dar comenzile losetup, dmsetup, mount și mkfs.ext4 cer privilegii administrative.

Verifică mediul înainte de a crea proiectul:

grep -E '^(NAME|VERSION_ID)=' /etc/os-release
uname -r
uname -m
systemd --version | head -n 1
gcc --version | head -n 1
dmsetup version
mkfs.ext4 -V 2>&1 | head -n 1
findmnt --version
df -h /var/lib

Un exemplu compatibil este:

NAME="Fedora Linux"
VERSION_ID="44"
7.2.11-200.fc44.x86_64
x86_64
systemd 259
gcc (GCC) 16.x
Library version: 1.02.x
mke2fs 1.47.x
findmnt from util-linux 2.x

Versiunile secundare ale GCC, LVM2, e2fsprogs și util-linux pot fi mai noi după actualizările Fedora. Cerințele rigide sunt kernel 7.0 sau mai nou pentru noua infrastructură generică și un filesystem care emite evenimentele urmărite. Pașii au fost fixați la kernel 7.2 și ext4.

Instalează pachetele într-un singur pas:

sudo dnf install -y gcc glibc-headers kernel-headers 
  device-mapper e2fsprogs util-linux systemd

Proiectul va avea această structură:

/opt/fserror-lab/
├── src/
│   └── fserror-monitor.c       # consumatorul fanotify pentru FAN_FS_ERROR
└── scripts/
    ├── lab-setup.sh            # creează imaginea, mapperul și volumul ext4
    ├── inject-io-error.sh      # comută temporar mapperul pe targetul error
    └── healthcheck.sh          # verifică serviciul și marca activă
/etc/systemd/system/
├── fserror-monitor.service     # procesul persistent de monitorizare
├── fserror-health.service      # verificarea periodică
└── fserror-health.timer        # programarea verificării
/etc/systemd/journald@fserror.conf.d/
└── retention.conf              # retenția journalului izolat

Pasul 1 — Compilăm consumatorul care decodează ERROR și FID

Programul trebuie să ceară explicit FAN_REPORT_FID. Documentația kernelului precizează această condiție pentru FAN_FS_ERROR. Folosim o marcă FAN_MARK_FILESYSTEM, nu o simplă supraveghere a directorului.

Creează directoarele fără a suprascrie un proiect existent:

sudo test ! -e /opt/fserror-lab
sudo install -d -m 0755 /opt/fserror-lab/src
sudo install -d -m 0755 /opt/fserror-lab/scripts

Fișierul /opt/fserror-lab/src/fserror-monitor.c:

#define _GNU_SOURCE

#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/fanotify.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

static volatile sig_atomic_t running = 1;

static void stop_monitor(int signo)
{
    (void)signo;
    running = 0;
}

static void print_timestamp(void)
{
    struct timespec ts;
    struct tm tm;
    char stamp[32];

    if (clock_gettime(CLOCK_REALTIME, &ts) == -1 ||
        gmtime_r(&ts.tv_sec, &tm) == NULL) {
        fputs("time=unknown ", stdout);
        return;
    }

    if (strftime(stamp, sizeof(stamp), "%Y-%m-%dT%H:%M:%S", &tm) == 0) {
        fputs("time=unknown ", stdout);
        return;
    }

    printf("time=%s.%03ldZ ", stamp, ts.tv_nsec / 1000000L);
}

static void print_fid(const struct fanotify_event_info_fid *fid)
{
    const struct file_handle *handle;
    unsigned int i;

    handle = (const struct file_handle *)fid->handle;

    printf("fsid=%08x:%08x handle_type=%d handle_bytes=%u handle=",
           (unsigned int)fid->fsid.val[0],
           (unsigned int)fid->fsid.val[1],
           handle->handle_type,
           handle->handle_bytes);

    for (i = 0; i < handle->handle_bytes; i++)
        printf("%02x", handle->f_handle[i]);
}

static int process_event(const struct fanotify_event_metadata *metadata)
{
    const char *base = (const char *)metadata;
    const struct fanotify_event_info_error *error_info = NULL;
    const struct fanotify_event_info_fid *fid_info = NULL;
    size_t offset = FAN_EVENT_METADATA_LEN;

    if (metadata->vers != FANOTIFY_METADATA_VERSION) {
        fprintf(stderr,
                "unsupported fanotify metadata version: got=%u expected=%un",
                metadata->vers, FANOTIFY_METADATA_VERSION);
        return -1;
    }

    if (metadata->mask & FAN_Q_OVERFLOW) {
        print_timestamp();
        puts("QUEUE_OVERFLOW events_may_be_missing=yes");
        fflush(stdout);
        return 0;
    }

    if (!(metadata->mask & FAN_FS_ERROR))
        return 0;

    while (offset + sizeof(struct fanotify_event_info_header) <=
           metadata->event_len) {
        const struct fanotify_event_info_header *header =
            (const struct fanotify_event_info_header *)(base + offset);

        if (header->len < sizeof(*header) ||
            offset + header->len > metadata->event_len) {
            fprintf(stderr,
                    "invalid fanotify information record: offset=%zu len=%u event_len=%un",
                    offset, header->len, metadata->event_len);
            return -1;
        }

        if (header->info_type == FAN_EVENT_INFO_TYPE_ERROR)
            error_info =
                (const struct fanotify_event_info_error *)header;
        else if (header->info_type == FAN_EVENT_INFO_TYPE_FID)
            fid_info = (const struct fanotify_event_info_fid *)header;

        offset += header->len;
    }

    print_timestamp();

    if (error_info != NULL) {
        int raw_error = error_info->error;
        int error_number = raw_error < 0 ? -raw_error : raw_error;

        printf("FS_ERROR raw=%d errno=%d message="%s" count=%u ",
               raw_error,
               error_number,
               strerror(error_number),
               error_info->error_count);
    } else {
        fputs("FS_ERROR error_record=missing ", stdout);
    }

    if (fid_info != NULL)
        print_fid(fid_info);
    else
        fputs("fid=missing", stdout);

    putchar('n');
    fflush(stdout);
    return 0;
}

int main(int argc, char **argv)
{
    int fanotify_fd;
    char buffer[8192] __attribute__((aligned(8)));

    if (argc != 2) {
        fprintf(stderr, "usage: %s MOUNTPOINTn", argv[0]);
        return EXIT_FAILURE;
    }

    signal(SIGTERM, stop_monitor);
    signal(SIGINT, stop_monitor);

    fanotify_fd = fanotify_init(FAN_CLASS_NOTIF | FAN_REPORT_FID,
                                O_RDONLY | O_LARGEFILE | O_CLOEXEC);
    if (fanotify_fd == -1) {
        perror("fanotify_init");
        return EXIT_FAILURE;
    }

    if (fanotify_mark(fanotify_fd,
                      FAN_MARK_ADD | FAN_MARK_FILESYSTEM,
                      FAN_FS_ERROR,
                      AT_FDCWD,
                      argv[1]) == -1) {
        perror("fanotify_mark");
        close(fanotify_fd);
        return EXIT_FAILURE;
    }

    printf("READY mount=%s mask=FAN_FS_ERROR report=FAN_REPORT_FIDn",
           argv[1]);
    fflush(stdout);

    while (running) {
        ssize_t bytes_read = read(fanotify_fd, buffer, sizeof(buffer));
        ssize_t remaining;
        struct fanotify_event_metadata *metadata;

        if (bytes_read == -1) {
            if (errno == EINTR)
                continue;
            perror("read");
            close(fanotify_fd);
            return EXIT_FAILURE;
        }

        if (bytes_read == 0)
            continue;

        remaining = bytes_read;
        metadata = (struct fanotify_event_metadata *)buffer;

        while (FAN_EVENT_OK(metadata, remaining)) {
            if (process_event(metadata) == -1) {
                close(fanotify_fd);
                return EXIT_FAILURE;
            }
            metadata = FAN_EVENT_NEXT(metadata, remaining);
        }
    }

    close(fanotify_fd);
    return EXIT_SUCCESS;
}

Compilează și instalează binarul:

sudo gcc -std=c17 -O2 -Wall -Wextra -Wpedantic 
  -o /usr/local/sbin/fserror-monitor 
  /opt/fserror-lab/src/fserror-monitor.c

sudo chown root:root /usr/local/sbin/fserror-monitor
sudo chmod 0755 /usr/local/sbin/fserror-monitor
/usr/local/sbin/fserror-monitor 2>&1 || true

Opțiunile -Wall, -Wextra și -Wpedantic activează verificări utile la compilare. Programul validează atât versiunea metadatelor fanotify, cât și lungimea fiecărei înregistrări, pentru a nu traversa date în afara evenimentului.

Trebuie să vezi:

usage: /usr/local/sbin/fserror-monitor MOUNTPOINT

Punct de control: dacă structura fanotify_event_info_error nu este declarată, verifică dacă kernel-headers și antetele glibc provin din Fedora 44; nu înlocui manual structurile ABI.

Pasul 2 — Construim volumul ext4 peste un mapper reversibil

Scriptul creează o imagine, atașează un loop device, construiește un target Device Mapper liniar și scrie un fișier de 64 MiB. Volumul este remontat read-only înaintea testului, astfel încât injectarea să nu întrerupă scrieri de jurnal ext4.

Fișierul /opt/fserror-lab/scripts/lab-setup.sh:

#!/usr/bin/env bash
set -euo pipefail

STATE_DIR=/var/lib/fserror-lab
IMAGE="$STATE_DIR/disk.img"
STATE_FILE="$STATE_DIR/state.env"
MOUNTPOINT=/srv/fserror-lab
MAPPER_NAME=fserror-lab
MAPPER_DEV="/dev/mapper/$MAPPER_NAME"
LOOP_DEV=
CREATED_MAPPER=0
MOUNTED=0

cleanup_on_error() {
    local status=$?

    if (( status == 0 )); then
        return
    fi

    if (( MOUNTED == 1 )); then
        umount "$MOUNTPOINT" || true
    fi
    if (( CREATED_MAPPER == 1 )); then
        dmsetup remove "$MAPPER_NAME" || true
    fi
    if [[ -n "$LOOP_DEV" ]]; then
        losetup -d "$LOOP_DEV" || true
    fi
    exit "$status"
}

trap cleanup_on_error EXIT

if [[ $EUID -ne 0 ]]; then
    echo "ERROR: rulează scriptul prin sudo" >&2
    exit 1
fi

if dmsetup info "$MAPPER_NAME" >/dev/null 2>&1; then
    echo "ERROR: mapperul $MAPPER_NAME există deja" >&2
    exit 1
fi

if mountpoint -q "$MOUNTPOINT"; then
    echo "ERROR: $MOUNTPOINT este deja un punct de montare" >&2
    exit 1
fi

if [[ -e "$IMAGE" || -e "$STATE_FILE" ]]; then
    echo "ERROR: starea laboratorului există deja în $STATE_DIR" >&2
    exit 1
fi

install -d -m 0700 "$STATE_DIR"
install -d -m 0755 "$MOUNTPOINT"
truncate -s 512M "$IMAGE"

LOOP_DEV=$(losetup --find --show "$IMAGE")
SECTORS=$(blockdev --getsz "$LOOP_DEV")

printf '0 %s linear %s 0n' "$SECTORS" "$LOOP_DEV" |
    dmsetup create "$MAPPER_NAME"
CREATED_MAPPER=1

mkfs.ext4 -q -F -L FSERROR_LAB "$MAPPER_DEV"
mount "$MAPPER_DEV" "$MOUNTPOINT"
MOUNTED=1

dd if=/dev/urandom of="$MOUNTPOINT/probe.bin" 
   bs=1M count=64 conv=fsync status=progress
sync
umount "$MOUNTPOINT"
MOUNTED=0

mount -o ro "$MAPPER_DEV" "$MOUNTPOINT"
MOUNTED=1

cat >"$STATE_FILE" <<EOF
LOOP_DEV=$LOOP_DEV
SECTORS=$SECTORS
MAPPER_NAME=$MAPPER_NAME
MOUNTPOINT=$MOUNTPOINT
EOF
chmod 0600 "$STATE_FILE"

findmnt --noheadings --output SOURCE,FSTYPE,OPTIONS "$MOUNTPOINT"
dmsetup table "$MAPPER_NAME"
stat --format='probe=%n bytes=%s' "$MOUNTPOINT/probe.bin"

trap - EXIT
echo "READY: laboratorul ext4 este montat read-only"

Activează și execută scriptul:

sudo chmod 0750 /opt/fserror-lab/scripts/lab-setup.sh
sudo /opt/fserror-lab/scripts/lab-setup.sh

blockdev --getsz întoarce dimensiunea în sectoare de 512 octeți, exact unitatea cerută de tabela Device Mapper. Tabela linear expune întreaga imagine fără transformări.

Trebuie să vezi un rezultat de această formă; numărul loop device-ului poate diferi:

/dev/mapper/fserror-lab ext4 ro,relatime
0 1048576 linear /dev/loop0 0
probe=/srv/fserror-lab/probe.bin bytes=67108864
READY: laboratorul ext4 este montat read-only

Punct de control: findmnt trebuie să arate ext4 și opțiunea ro; nu continua dacă sursa este un disc real sau dacă tabela nu începe cu linear.

Pasul 3 — Pornim monitorul cu o capabilitate delimitată și journal propriu

Marca FAN_MARK_FILESYSTEM împreună cu raportarea FID necesită privilegii. Procesul nu rulează cu UID 0: primește CAP_SYS_ADMIN prin systemd. Aceasta rămâne o capabilitate foarte puternică, motiv pentru care binarul este deținut de root, nu este editabil de utilizatorul serviciului și rulează cu protecții suplimentare.

Creează utilizatorul:

sudo useradd --system --home-dir / --shell /usr/sbin/nologin 
  fserror-mon 2>/dev/null || true

Fișierul /etc/systemd/system/fserror-monitor.service:

[Unit]
Description=Monitor FAN_FS_ERROR pentru volumul de laborator
After=local-fs.target
RequiresMountsFor=/srv/fserror-lab
ConditionPathIsMountPoint=/srv/fserror-lab

[Service]
Type=simple
User=fserror-mon
Group=fserror-mon
ExecStart=/usr/local/sbin/fserror-monitor /srv/fserror-lab
Restart=on-failure
RestartSec=3s
AmbientCapabilities=CAP_SYS_ADMIN
CapabilityBoundingSet=CAP_SYS_ADMIN
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictRealtime=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
SystemCallArchitectures=native
LogNamespace=fserror

[Install]
WantedBy=multi-user.target

Un journal namespace separă logurile monitorului de journalul principal și permite o politică proprie de retenție. Fișierul /etc/systemd/journald@fserror.conf.d/retention.conf:

[Journal]
Storage=persistent
Compress=yes
SystemMaxUse=256M
MaxRetentionSec=14day

Înainte de creare, verifică să nu existe configurații cu același nume:

sudo test ! -e /etc/systemd/system/fserror-monitor.service
sudo test ! -e /etc/systemd/journald@fserror.conf.d/retention.conf
sudo install -d -m 0755 /etc/systemd/journald@fserror.conf.d

Salvează fișierele de mai sus, apoi pornește serviciul:

sudo systemctl daemon-reload
sudo systemctl enable --now fserror-monitor.service
sudo systemctl is-active fserror-monitor.service
sudo journalctl --namespace=fserror 
  -u fserror-monitor.service --no-pager -n 10

Trebuie să vezi:

active
READY mount=/srv/fserror-lab mask=FAN_FS_ERROR report=FAN_REPORT_FID

ProtectSystem=strict face arborele de fișiere read-only pentru proces, iar MemoryDenyWriteExecute=yes împiedică mapările simultan inscriptibile și executabile. LogNamespace=fserror trimite ieșirea serviciului către instanța sa de journald.

Punct de control: dacă lipsește mesajul READY, citește imediat systemctl status fserror-monitor.service și journalul namespace-ului înainte de a injecta eroarea.

Pasul 4 — Comutăm controlat numai volumul de laborator pe EIO

Avertisment: următorul script suspendă temporar dispozitivul /dev/mapper/fserror-lab. Nu modifica MAPPER_NAME pentru a indica un volum de producție. Scriptul acceptă numai numele și punctul de montare salvate de etapa de creare.

Fișierul /opt/fserror-lab/scripts/inject-io-error.sh:

#!/usr/bin/env bash
set -euo pipefail

STATE_FILE=/var/lib/fserror-lab/state.env
SWITCHED=0

if [[ $EUID -ne 0 ]]; then
    echo "ERROR: rulează scriptul prin sudo" >&2
    exit 1
fi

if [[ ! -r "$STATE_FILE" ]]; then
    echo "ERROR: lipsește $STATE_FILE" >&2
    exit 1
fi

source "$STATE_FILE"

if [[ "$MAPPER_NAME" != "fserror-lab" ||
      "$MOUNTPOINT" != "/srv/fserror-lab" ]]; then
    echo "ERROR: starea laboratorului nu corespunde țintei permise" >&2
    exit 1
fi

CURRENT_TABLE=$(dmsetup table "$MAPPER_NAME")
EXPECTED_TABLE="0 $SECTORS linear $LOOP_DEV 0"

if [[ "$CURRENT_TABLE" != "$EXPECTED_TABLE" ]]; then
    echo "ERROR: tabela mapperului nu este cea liniară a laboratorului" >&2
    echo "GĂSIT: $CURRENT_TABLE" >&2
    exit 1
fi

restore_linear() {
    local status=$?

    if (( SWITCHED == 1 )); then
        dmsetup suspend "$MAPPER_NAME" || true
        printf '0 %s linear %s 0n' "$SECTORS" "$LOOP_DEV" |
            dmsetup load "$MAPPER_NAME" || true
        dmsetup resume "$MAPPER_NAME" || true
    fi

    exit "$status"
}

trap restore_linear EXIT INT TERM

dmsetup suspend "$MAPPER_NAME"
printf '0 %s errorn' "$SECTORS" | dmsetup load "$MAPPER_NAME"
dmsetup resume "$MAPPER_NAME"
SWITCHED=1

if dd if="$MOUNTPOINT/probe.bin" of=/dev/null 
      bs=1M iflag=direct status=none; then
    echo "ERROR: citirea a reușit, deși targetul error era activ" >&2
    exit 1
else
    echo "EXPECTED: citirea directă a primit o eroare I/O"
fi

sleep 2

dmsetup suspend "$MAPPER_NAME"
printf '0 %s linear %s 0n' "$SECTORS" "$LOOP_DEV" |
    dmsetup load "$MAPPER_NAME"
dmsetup resume "$MAPPER_NAME"
SWITCHED=0

dmsetup table "$MAPPER_NAME"
dd if="$MOUNTPOINT/probe.bin" of=/dev/null 
   bs=1M iflag=direct status=none

trap - EXIT
echo "RESTORED: tabela liniară și citirea directă funcționează"

Rulează testul:

sudo chmod 0750 /opt/fserror-lab/scripts/inject-io-error.sh
sudo /opt/fserror-lab/scripts/inject-io-error.sh

iflag=direct cere O_DIRECT, astfel încât verificarea să nu fie satisfăcută doar din page cache. Targetul error răspunde cu eroare tuturor operațiilor adresate lui. Capcana trap încearcă restaurarea tabelei liniare la orice ieșire ulterioară comutării.

Ieșirea așteptată include eroarea dd; numărul de octeți la care apare poate varia:

dd: error reading '/srv/fserror-lab/probe.bin': Input/output error
EXPECTED: citirea directă a primit o eroare I/O
0 1048576 linear /dev/loop0 0
RESTORED: tabela liniară și citirea directă funcționează

Punct de control: indiferent de rezultatul testului, rulează sudo dmsetup table fserror-lab; primul target trebuie să fie din nou linear, nu error.

Pasul 5 — Verificăm traseul complet și instalăm supravegherea periodică

Citește numai evenimentele monitorului apărute în ultimele cinci minute:

sudo journalctl --namespace=fserror 
  -u fserror-monitor.service 
  --since '-5 minutes' --no-pager

Exemplul de rezultat arată astfel; timestampul, fsid, handle-ul și contorul pot diferi:

READY mount=/srv/fserror-lab mask=FAN_FS_ERROR report=FAN_REPORT_FID
time=2026-10-05T12:34:56.123Z FS_ERROR raw=-5 errno=5 message="Input/output error" count=1 fsid=4f31a2c0:9018be77 handle_type=1 handle_bytes=12 handle=7a0000000000000000000000

Acesta este testul de capăt: dd a primit eroarea operației, fserror a propagat raportarea către fsnotify, fanotify a livrat evenimentul, iar serviciul l-a decodat în userspace. Un câmp fid=missing poate apărea pentru erori care nu pot fi asociate unui obiect identificabil; eroarea rămâne totuși relevantă la nivel de filesystem.

Pentru verificarea periodică, fișierul /opt/fserror-lab/scripts/healthcheck.sh:

#!/usr/bin/env bash
set -euo pipefail

SERVICE=fserror-monitor.service
MOUNTPOINT=/srv/fserror-lab

if ! mountpoint -q "$MOUNTPOINT"; then
    echo "HEALTH status=failed reason=mountpoint_missing path=$MOUNTPOINT"
    exit 1
fi

if ! systemctl is-active --quiet "$SERVICE"; then
    echo "HEALTH status=failed reason=service_inactive unit=$SERVICE"
    exit 1
fi

MAIN_PID=$(systemctl show "$SERVICE" --property=MainPID --value)

if [[ -z "$MAIN_PID" || "$MAIN_PID" == "0" ]]; then
    echo "HEALTH status=failed reason=main_pid_missing unit=$SERVICE"
    exit 1
fi

echo "HEALTH status=ok unit=$SERVICE pid=$MAIN_PID mount=$MOUNTPOINT"

Fișierul /etc/systemd/system/fserror-health.service:

[Unit]
Description=Verifică monitorul FAN_FS_ERROR
After=fserror-monitor.service

[Service]
Type=oneshot
ExecStart=/opt/fserror-lab/scripts/healthcheck.sh
LogNamespace=fserror
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes

Fișierul /etc/systemd/system/fserror-health.timer:

[Unit]
Description=Verificare periodică pentru fserror-monitor

[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
Persistent=true
Unit=fserror-health.service

[Install]
WantedBy=timers.target

Activează verificarea:

sudo chmod 0750 /opt/fserror-lab/scripts/healthcheck.sh
sudo systemctl daemon-reload
sudo systemctl enable --now fserror-health.timer
sudo systemctl start fserror-health.service
sudo systemctl list-timers fserror-health.timer --no-pager
sudo journalctl --namespace=fserror 
  -u fserror-health.service --no-pager -n 5

Trebuie să vezi un mesaj asemănător:

HEALTH status=ok unit=fserror-monitor.service pid=1842 mount=/srv/fserror-lab

Timerul confirmă existența mountpointului, starea serviciului și un PID principal nenul. Nu confirmă că dispozitivul este sănătos; aceasta cere instrumentele sistemului de fișiere și analiza operațiilor care au eșuat.

Pentru investigații temporare, eBPF poate adăuga context despre call path, dar nu înlocuiește ABI-ul fanotify și trebuie controlat atent. Într-un mediu cu politici stricte, ghidul despre verificarea programelor eBPF semnate oferă modelul de guvernanță potrivit.

Șase mesaje care localizează defectul laboratorului

fanotify_init: Operation not permitted

Procesul nu are privilegiul cerut de grupul fanotify cu FID. Confirmă că serviciul păstrează capabilitatea:

sudo systemctl show fserror-monitor.service 
  -p User -p AmbientCapabilities -p CapabilityBoundingSet
sudo systemctl restart fserror-monitor.service

Nu rezolva problema transformând binarul în setuid root.

fanotify_mark: Invalid argument

Kernelul activ sau combinația de filesystem și mască nu acceptă marca solicitată. Confirmă versiunea, mountpointul și tipul filesystemului:

uname -r
findmnt -no TARGET,FSTYPE,OPTIONS /srv/fserror-lab
grep -w FAN_FS_ERROR /usr/include/linux/fanotify.h

Pe acest laborator trebuie să apară kernel 7.2, ext4 și constanta din antet.

fanotify_mark: No such device

Mountpointul nu este asociat unui filesystem care poate fi marcat prin fsnotify sau serviciul a pornit după demontare. Refă montarea și repornește unitatea:

sudo findmnt /srv/fserror-lab
sudo systemctl restart fserror-monitor.service

dd: error reading '/srv/fserror-lab/probe.bin': Input/output error

În interiorul inject-io-error.sh, acesta este rezultatul intenționat, nu defectul testului. Devine o problemă dacă persistă după mesajul RESTORED.

sudo dmsetup table fserror-lab
sudo dd if=/srv/fserror-lab/probe.bin of=/dev/null 
  bs=1M iflag=direct status=none

Tabela trebuie să conțină linear.

ERROR: tabela mapperului nu este cea liniară a laboratorului

Scriptul a refuzat să schimbe o configurație neașteptată. Nu forța executarea. Inspectează starea salvată și tabela activă:

sudo cat /var/lib/fserror-lab/state.env
sudo dmsetup table fserror-lab
sudo losetup --list

Dacă tabela este error, reconstruiește manual tabela linear numai cu valorile din state.env, apoi reia testul.

Eroarea apare în dd, dar journalul nu conține FS_ERROR

Mai întâi confirmă că monitorul afișase READY înaintea injectării și că citești namespace-ul corect:

sudo systemctl is-active fserror-monitor.service
sudo journalctl --namespace=fserror 
  -u fserror-monitor.service --no-pager

Dacă serviciul este activ, kernelul sau call path-ul testat nu a emis notificarea. FAN_FS_ERROR nu este o copie universală a tuturor erorilor bloc. Verifică versiunea kernelului și suportul filesystemului în documentația acelei versiuni; nu declara monitorizarea funcțională doar pentru că dd a întors EIO.

Demontăm întâi filesystemul, apoi mapperul și loop device-ul

Avertisment: ordinea contează. Încercarea de a elimina mapperul cât timp ext4 este montat produce Device or resource busy.

Oprește componentele și citește starea înainte de ștergere:

sudo systemctl disable --now fserror-health.timer
sudo systemctl stop fserror-health.service 2>/dev/null || true
sudo systemctl disable --now fserror-monitor.service

sudo bash -c '
set -euo pipefail
source /var/lib/fserror-lab/state.env

if dmsetup table "$MAPPER_NAME" | grep -qw error; then
    dmsetup suspend "$MAPPER_NAME"
    printf "0 %s linear %s 0n" "$SECTORS" "$LOOP_DEV" |
        dmsetup load "$MAPPER_NAME"
    dmsetup resume "$MAPPER_NAME"
fi

umount "$MOUNTPOINT"
dmsetup remove "$MAPPER_NAME"
losetup -d "$LOOP_DEV"
'

Șterge numai fișierele create de laborator:

sudo rm -f /etc/systemd/system/fserror-monitor.service
sudo rm -f /etc/systemd/system/fserror-health.service
sudo rm -f /etc/systemd/system/fserror-health.timer
sudo rm -f /usr/local/sbin/fserror-monitor
sudo rm -rf /etc/systemd/journald@fserror.conf.d
sudo rm -rf /var/lib/fserror-lab
sudo rm -rf /opt/fserror-lab
sudo rmdir /srv/fserror-lab 2>/dev/null || true
sudo userdel fserror-mon 2>/dev/null || true
sudo systemctl daemon-reload
sudo systemctl reset-failed

Namespace-ul persistent poate păstra loguri sub /var/log/journal. Nu șterge manual directoare journal după nume presupuse; verifică mai întâi cu journalctl --namespace=fserror --disk-usage și aplică politica operațională a sistemului.

Lista de acceptanță pentru monitorul FAN_FS_ERROR

  • ☐ Kernelul activ este 7.2.x sau o versiune ulterioară validată.
  • ☐ /srv/fserror-lab este ext4, read-only și provine din /dev/mapper/fserror-lab.
  • ☐ dmsetup table fserror-lab arată targetul linear înainte și după test.
  • ☐ Binarul este deținut de root și are modul 0755.
  • ☐ Serviciul rulează ca fserror-mon, nu ca root.
  • ☐ Journalul namespace-ului conține mesajul READY.
  • ☐ Testul controlat produce Input/output error în dd.
  • ☐ Același test produce un eveniment FS_ERROR cu errno=5.
  • ☐ Citirea directă reușește după restaurarea tabelei.
  • ☐ fserror-health.timer este activ și emite HEALTH status=ok.

Trei extinderi care păstrează semnalul verificabil

Prima extindere este înlocuirea mesajului text cu un obiect JSON scris de monitor, păstrând câmpurile errno, error_count, fsid și handle-ul. Un colector local poate valida schema înainte de expediere.

A doua este asocierea file handle-ului cu inventarul intern al volumului. open_by_handle_at() cere privilegii și poate eșua dacă obiectul nu mai există, deci rezoluția trebuie tratată drept informație suplimentară, nu condiție pentru înregistrarea erorii.

A treia este un test separat pe XFS, folosind altă imagine și același consumator fanotify. Nu amesteca rezultatele ext4 și XFS: sistemele de fișiere nu raportează neapărat aceleași clase de erori din aceleași locuri.

Întrebări frecvente

Monitorul confirmă că o scriere sau o citire a reușit?

Nu. Aplicația trebuie să verifice rezultatul propriei operații. FAN_FS_ERROR notifică monitorul că filesystemul a observat o problemă, fără să înlocuiască semantica apelului I/O.

De ce nu folosim doar dmesg sau expresii regulate peste journalul kernelului?

Mesajele text diferă între subsisteme și versiuni. fanotify oferă un eveniment structurat cu eroare, contor și, când este disponibil, un identificator FID.

Poate serviciul să ruleze fără CAP_SYS_ADMIN?

Nu în configurația acestui proiect. Marca la nivel de filesystem și raportarea FID cer privilegiul respectiv, motiv pentru care capabilitatea este acordată unui serviciu dedicat și delimitată prin systemd.

Testul poate afecta discul sistemului?

Scriptul operează numai asupra mapperului fserror-lab, creat peste o imagine de 512 MiB, și refuză o tabelă neașteptată. Totuși, laboratorul trebuie executat într-o mașină de test, iar numele țintei nu trebuie schimbat către un volum real.

Surse

Facebook
X
WhatsApp
fserror și fsnotify în Linux: drumul erorilor I/O și al metadata către userspace

Te-ar putea interesa si: