wc -c reports too few bytes when its standard input is an inherited,
seekable regular file whose size is an exact multiple of the system page
size and whose descriptor is positioned at a non-zero offset. (Named file
operands are unaffected: wc opens them itself at offset 0. Only a
pre-positioned inherited stdin triggers it.) This is a documented, supported
case -- the comment at src/wc.c (~lines 400-408) states wc must report fewer
than st_size bytes when stdin is not at the beginning, and bug#61300 was
accepted on that same premise.

Reproducer (portable; P = page size):

    P=$(getconf PAGESIZE)
    head -c $((2 * P)) /dev/zero > f            # size = 2 pages
    (dd bs=1 skip=100 count=0 2>/dev/null; wc -c) < f

On a 4096-byte-page machine this prints 7992; the correct answer is 8092
(8192 - 100). Cross-check: `tail -c +101 f | wc -c` prints 8092.

More strikingly, at a larger offset the count collapses toward 1:

    (dd bs=1 skip=4096 count=0 2>/dev/null; wc -c) < f   # prints 1,
should be 4096

Affected: reproduced on 9.11 and 9.12, and the code is byte-identical in git
master (as of 2026-10-07); also reproduced on the 8.32 system binary. The
defective seek was introduced by commit 2662702b (2014-10-07, the fix for
bug#18621) and first released in 8.24; it affected every sized file at a
non-zero offset until commit e17e5f40 (released 8.27, 2016-12) narrowed it to
the page-multiple case. So every release from 8.24 through 9.12 is affected.

Root cause -- src/wc.c, the byte-only fast path, the branch taken when the
size is an exact multiple of the page size:

    else
      {
        off_t hi_pos = (end_pos
                        - end_pos % (STP_BLKSIZE (&fstatus->st) + 1));
        if (0 <= current_pos && current_pos < hi_pos
            && 0 <= lseek (fd, hi_pos, SEEK_CUR))      /* <-- bug */
          bytes = hi_pos - current_pos;
      }

hi_pos is an ABSOLUTE file position (st_size rounded down to a multiple of
st_blksize + 1 -- e.g. 4097 for an 8192-byte file on a 4096-block filesystem),
but the seek uses SEEK_CUR, i.e. RELATIVE to the current position. When the
inherited offset (current_pos) is non-zero the descriptor lands at
current_pos + hi_pos instead of at hi_pos, so the following read loop reads
current_pos too few bytes. Combined with the pre-seeded
`bytes = hi_pos - current_pos`, the total comes to st_size - 2*current_pos.
When current_pos + hi_pos >= st_size the relative seek lands at or past EOF,
the read returns 0, and the result collapses to hi_pos - current_pos (the
"prints 1" case above). The sibling non-page-multiple branch a few lines up
is correct precisely because its SEEK_CUR argument (end_pos - current_pos) is
a genuine relative length.

strace of the failing run (offset 100, fd 0):
    lseek(0, 0, SEEK_CUR)    = 100
    lseek(0, 4097, SEEK_CUR) = 4197        <- relative seek, absolute target
    read(0, ..., 262144)     = 3995
    => 3997 (pre-seeded) + 3995 = 7992

Related but NOT duplicates: bug#61300 (fixed in 9.3) fixed the OTHER branch's
failure to advance the fd -- the count was correct there; this is a wrong
count in the page-multiple branch. bug#18621 is where this seek originated.

Fix: seek to the absolute target, i.e. change that line to

            && 0 <= lseek (fd, hi_pos, SEEK_SET))

(equivalently `lseek (fd, hi_pos - current_pos, SEEK_CUR)`). Verified against
the full offset matrix on a locally built 9.11: all offsets then correct
(8092 / 8192 / 8093 for the three cases above; the offset-4096 case -> 4096)
with no regression in tests/wc/wc-proc.sh.

Reply via email to