--- id: "bug-1498" status: "fixed" status_updated: "2026-09-24" status_note: "" component: "scheduler" severity: "security" timestamp: "2026-08-05T00:00:00Z" last_seen: "2026-09-19T09:00:00Z" occurrences: 1 files: ["src/scheduler/src/recording.c"] tags: ["scheduler-core", "task-9.2", "recording.c", "toctou", "hardlink", "O_TRUNC", "check-after-side-effect", "sched_record_start"] related: [] legacy: true --- # bug-1498: test_toctou_hardlink_planted_after_validation_rejected FAILED: Expected Non-NULL -- fgets() on the… ## Description test_toctou_hardlink_planted_after_validation_rejected FAILED: Expected Non-NULL -- fgets() on the outside 'victim' file returned NULL (file was 0 bytes) after the TOCTOU regression test ran, even though sched_record_start()/the reproduced open-hardening sequence both correctly REJECTED the hard-link swap and returned an error. ## Environment _Not recorded (migrated from buglog.json)._ ## Reproduction _Not recorded (migrated from buglog.json)._ ## Root cause sched_record_start()'s TOCTOU-hardened open used O_WRONLY|O_CREAT|O_TRUNC|O_NOFOLLOW. O_TRUNC truncates the target atomically as PART OF the open(2) syscall itself, before any userspace code -- including the fstat()+st_nlink==1 safety check right after open() -- ever runs. Against a hard-linked victim (same inode as the outside file, no symlink involved so O_NOFOLLOW never triggers), open() succeeds and O_TRUNC immediately zeroes the shared inode's content; the subsequent st_nlink>1 check then correctly rejects and closes the fd, but the outside victim file was already corrupted by that point. The check ran too late to matter -- classic 'check-after-side-effect' TOCTOU-adjacent bug, caught by the test's own outside-file-content assertion (added specifically to prove containment, not just that the return code was negative). ## Fix Removed O_TRUNC from the initial open() flags (now O_WRONLY|O_CREAT|O_NOFOLLOW). After fstat()+st_nlink==1 passes, explicitly call ftruncate(fd, 0) to get the same truncate-on-legitimate-overwrite behavior, but only once safety is verified. Same fix applied to the test's own toctou_hardened_open() helper (a verbatim reproduction of the production sequence, used because a literal multi-threaded race against sched_record_start()'s few-instruction internal window would be flaky). ## Verification _Not recorded (migrated from buglog.json)._ ## References _Not recorded (migrated from buglog.json)._