From 0079d4ce63dac71e62295e66b8ec5d2ac17c53ce Mon Sep 17 00:00:00 2001 From: Eduardo Rodrigues <16357187+eduardomourar@users.noreply.github.com> Date: Sat, 1 Aug 2026 22:26:05 +0100 Subject: [PATCH] tests(tail): fix race in untailable-symlink test The test replaced the watched file with a symlink via remove() followed by symlink_file(), letting the poller observe a transient "file missing" state and report "has become inaccessible" instead of "replaced with an untailable symbolic link". It also asserted the process was alive 500ms after spawn without confirming tail's initial read had completed, so under scheduler contention the swap could land before tail's first (legitimately symlink-following) open of the file, leaking the secret before the follow-loop's guard was active. Wait for the initial content to appear in stdout before swapping, and perform the swap as an atomic rename over a freshly created symlink so the intermediate missing-file state is never observable. --- tests/by-util/test_tail.rs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/tests/by-util/test_tail.rs b/tests/by-util/test_tail.rs index 11480021a0..8c76f97d6e 100644 --- a/tests/by-util/test_tail.rs +++ b/tests/by-util/test_tail.rs @@ -1631,9 +1631,28 @@ fn test_follow_name_replaced_by_symlink_is_untailable() { .run_no_wait(); p.make_assertion_with_delay(500).is_alive(); + // Wait until tail's *initial* read of "watched" has actually completed + // (i.e. the pre-existing content has been printed) before swapping it + // for a symlink. Otherwise, under scheduler contention, tail's initial + // open (which legitimately follows symlinks, like any other file open) + // could race with the swap and open the symlink's target on its very + // first read -- before the untailable-symlink guard (which only applies + // to the follow loop) is even active. That would be a test timing bug, + // not the vulnerability this test is meant to catch. + for _ in 0..50 { + if p.stdout_all().contains("visible") { + break; + } + p.delay(20); + } + // Swap the watched file for a symlink pointing at a secret file. - at.remove("watched"); - at.symlink_file("secret", "watched"); + // Create the symlink under a temporary name and rename it into place + // atomically, so the poller never observes an intermediate state where + // "watched" is missing (which would race with the poll interval and be + // reported as "has become inaccessible" instead). + at.symlink_file("secret", "watched.tmp"); + at.rename("watched.tmp", "watched"); p.delay(1000); p.kill()