in_tail: files falsely detected as rotated on macOS with threaded inputs, causing duplicate records

Abierto Apto para principiantes
#12,780 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

@kevin9327 ya está trabajando en esto.

Desde el 12/11/2026.

  • #12782 de @kevin9327 — abierto

Evaluación

Dificultad
2/5
Tiempo estimado
1-3 horas
Aptitud para principiantes
75/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
c, macos
Área
backend

Línea de trabajo

Empieza en plugins/in_tail/tail_file.h, en flb_tail_target_file_name_cmp(), y compara el manejo de los nombres base en macOS con la corrección para Windows de PR #11553. Reproduce el problema usando las dos entradas tail con y sin hilos y los archivos estáticos descritos en el issue. Se considera terminado cuando la compilación de macOS ya no informa de rotaciones falsas ni vuelve a leer registros en el escenario de estrés; el issue no indica ningún archivo de prueba.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

Describe the bug

On macOS, when at least one tail input has threaded on, in_tail intermittently decides that a file was rotated when it was not. The file is then picked up again by the next refresh scan and re-read from offset 0, so its records are sent again. Both the threaded input and other (main-thread) tail inputs are affected.

The cause is flb_tail_target_file_name_cmp() in plugins/in_tail/tail_file.h, which calls libc basename(3) twice and compares the results. On macOS, Apple's libc basename() returns a pointer to a single static buffer shared by the whole process. When a threaded tail input and a main-thread tail input run this compare at the same time, one thread's result overwrites the other's, strcmp() sees a mismatch, and the file is treated as rotated.

This is the same class of problem that PR #11553 fixed for Windows (thread-local compat buffer). macOS uses Apple libc and is still affected. As of 2026-11-10, master has the same code as v4.2.3.

To Reproduce

Two tail inputs, each tailing 50 static one-line files that never change, null output. Any rotation counted is therefore false.

Setup:

R=/private/tmp/fbrepro
mkdir -p $R/a $R/b
for i in $(seq -w 1 50); do
  echo "static line $i" > $R/a/f$i.log
  echo "static line $i" > $R/b/f$i.log
done

fluent-bit.conf:

[SERVICE]
    flush        1
    log_level    info
    http_server  on
    http_listen  127.0.0.1
    http_port    2099

[INPUT]
    name            tail
    alias           tail_threaded
    path            /private/tmp/test/a/*.log
    read_from_head  on
    threaded        on

[INPUT]
    name            tail
    alias           tail_main
    path            /private/tmp/test/b/*.log
    read_from_head  on

[OUTPUT]
    name   null
    match  *

Run it, wait a few minutes, then read the counters:

fluent-bit -c fluent-bit.conf
curl -s http://127.0.0.1:2099/api/v1/metrics/prometheus \
  | grep -E '^fluentbit_input_(files_rotated_total|records_total)'

Results on Fluent Bit v4.2.3, macOS 27.2 arm64, 10 minutes, plus a control run of the same config with threaded off on both inputs, run at the same time:

Run files_rotated_total (tail_threaded / tail_main) records_total (expected 50 / 50)
threaded on on the first input 16 / 12 65 / 62
Control: threaded off 0 / 0 50 / 50

The log shows rotations where the old and new paths are identical:

[info] [input:tail:tail_threaded] inode=210871275 handle rotation(): /private/tmp/test/A/a/f22.log => /private/tmp/test/A/a/f22.log
[info] [input:tail:tail_main] inode=210871272 handle rotation(): /private/tmp/test/A/b/f20.log => /private/tmp/test/A/b/f20.log

Expected behavior

Files that are not renamed, moved or replaced are never reported as rotated, and their records are read once.

Actual behavior

Files are reported as rotated at random. Each false rotation is followed by a full re-read of the file at the next refresh scan (60 s by default), sending duplicate records.

Root cause

flb_tail_target_file_name_cmp() (tail_file.h L80,
L104-L105), non-Windows branch:

base_a = flb_strdup(basename(name_a));
...
base_b = basename(name_b);
ret = strcmp(base_a, base_b);
...
/* FYI: 'base_b' never points to a new allocation, no flb_free is needed */

The comment, and the lack of locking, assume glibc behavior, where basename() returns a pointer into its argument. POSIX says basename() need not be thread-safe, and Apple's implementation returns a pointer to one process-wide static buffer. Calling Apple libc basename() on two different paths returns the same pointer, and the first result is overwritten:

basename(a) -> 0x1015153c0 b'first.log'
basename(b) -> 0x1015153c0 b'second.log'
same pointer: True | first result now reads: b'second.log'

(Output from calling libc basename() through Python ctypes on macOS 27.2 arm64.)

A tail input with threaded on runs its fs-check timer on its own thread; other tail inputs run theirs on the main thread. If the two compares overlap, either thread can see the other's basename. flb_tail_file_is_rotated() (tail_file.c L1913) then returns true and flb_tail_file_rotated() runs: the file's DB entry is renamed, the refresh scan finds the original path again as a new file, and it is read from offset 0.

Why particular inputs are hit
flb_input_collectors_start() (flb_input.c L1839-L1865) arms collectors in config order, and signals each threaded input to start its own collectors from inside that loop (flb_input_thread.c L744). The threaded input's timer and the next main-thread
tail input's timer therefore start at almost the same moment with the same period, and keep firing together. In our production config, only the threaded input and the input directly after it in config order showed false rotations; all other tail inputs showed none.

Additional evidence (production, macOS, v4.2.3)

  • Over 18.8 hours on one host: input A (threaded, hourly rotation, about 19 real rotations) reported 54 rotations; input B (next in config order, 1 real rotation at midnight) reported 10; the other 17 tail
    inputs reported 0.
  • Input A re-read about 62 GB in about 7.5 hours.
  • Across our macOS hosts, input B's records reach the backend about 7.8x on average. Linux and Windows hosts with the same inputs show 1x.
  • fs_usage caught a "rotation" while the path from fcntl(F_GETPATH), polled every 20 ms, never changed, and there was no rename, link or write on the file.
  • A Python model of the compare logic produced about 0.3% false mismatches with 2 threads and 0 with 1 thread.
  • Removing threaded on from input A: 0 false rotations over 5h45m. Re-enabling it for 30 minutes: 5 false rotations.
  • 20-minute side-by-side stress test (20 tail inputs on 20 static files, 10 threaded, null output): stock 4.2.3 had 366 false rotations on threaded inputs and 43 on non-threaded inputs, with 402 records
    re-read. A 4.2.3 build with an equivalent patch had 0 false rotations and 0 records re-read.

Proposed fix

On __APPLE__, use the reentrant basename_r(3) with per-call stack buffers for both names. Linux and Windows are unchanged. This applies cleanly to v4.2.3 and current master:

diff --git a/plugins/in_tail/tail_file.h b/plugins/in_tail/tail_file.h
--- a/plugins/in_tail/tail_file.h
+++ b/plugins/in_tail/tail_file.h
@@ -35,6 +35,11 @@
 #include "win32.h"
 #endif

+#ifdef __APPLE__
+#include <libgen.h>
+#include <sys/param.h>
+#endif
+
 #ifdef FLB_HAVE_REGEX
 #define FLB_HASH_TABLE_SIZE 50
 #endif
@@ -69,6 +74,16 @@
     char *name_b = NULL;
     char *base_a = NULL;
     char *base_b = NULL;
+#if defined(__APPLE__)
+    /*
+     * Apple's basename(3) returns a pointer to one static buffer shared by
+     * the whole process, so a threaded tail input and the main thread can
+     * overwrite each other's result mid-compare and report a false rotation.
+     * Use the reentrant basename_r(3) with per-call buffers instead.
+     */
+    char base_buf_a[MAXPATHLEN];
+    char base_buf_b[MAXPATHLEN];
+#endif

     name_a = flb_strdup(name);
     if (!name_a) {
@@ -77,7 +92,14 @@
         goto out;
     }

+#if defined(__APPLE__)
+    base_a = basename_r(name_a, base_buf_a);
+    if (base_a) {
+        base_a = flb_strdup(base_a);
+    }
+#else
     base_a = flb_strdup(basename(name_a));
+#endif
     if (!base_a) {
         flb_errno();
         ret = -1;
@@ -101,7 +123,16 @@
         ret = -1;
         goto out;
     }
+#if defined(__APPLE__)
+    base_b = basename_r(name_b, base_buf_b);
+    if (!base_b) {
+        flb_errno();
+        ret = -1;
+        goto out;
+    }
+    }
+#else
     base_b = basename(name_b);
+#endif
 #endif

A portable alternative would be to drop libc basename() here and use a small helper that returns a pointer into the already-duplicated name, for example (untested sketch; Windows would also need to handle
\):

static inline char *tail_basename(char *path)
{
    char *p = strrchr(path, '/');
    return p ? p + 1 : path;
}

That avoids any shared static state on every platform, and would also cover other libcs with the same behavior.

Workaround

Do not set threaded on on tail inputs on macOS.

Your Environment

  • Version used: Fluent Bit v4.2.3 (git commit 40c2f276057ed518d1b75c184a133afd1a13d61c)
  • Operating System and version: macOS on Apple Silicon (repro above on macOS 27.2, arm64)
  • Configuration: see "To Reproduce"
  • Filters and plugins: in_tail (stat-based file watching on macOS), out_null in the repro

Related

  • #11553 compat: fix basename buffer to be thread-local on Windows (same bug class, Windows only)
  • #11357 Threaded mode always re-reads files from beginning regardless of DB offset or Read_from_Head setting (different mechanism)
  • #11460 in_tail: Serialize DB operation for preventing resource collisions on threaded mode (different mechanism)
Lenguaje dominante
C
Estrellas
8.2k
Forks
2k
Merge medio
3 d 20 h
PR fusionados (30 d)
99

Preparar el entorno

Abrir en Codespaces

Inicia el contenedor de desarrollo del proyecto en tu navegador, con tu propia cuenta de GitHub.

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de fluent/fluent-bit

Todos los issues de fluent/fluent-bit

Issues similares

Más issues de C

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.