Skip to content

Linux io::copy of Take resets Take::len via set_limit, breaking Seek #617

Description

@SebTardif

Summary

On Linux, io::copy of a Take<File> (or other specialized reader) calls Take::set_limit to decrement the remaining cap. Since 1.89, set_limit also resets Take::len, which Seek uses as the original take length.

After a specialized copy, stream_position() reports 0 instead of the bytes copied, and seek(SeekFrom::Start(0)) does not rewind the inner reader.

Public trigger

Stable Linux std (Take::Seek since 1.89):

use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom};

fn main() -> io::Result<()> {
    let mut src = File::open("in.bin")?;
    let mut dst = File::create("out.bin")?;
    let mut take = src.take(1_000_000);
    io::copy(&mut take, &mut dst)?;
    assert_eq!(take.stream_position()?, 1_000_000); // Linux: 0
    take.seek(SeekFrom::Start(0))?;                 // does not rewind inner File
    Ok(())
}

Generic Read (macOS/Windows, or a reader without kernel_copy specialization) only decrements limit and leaves len alone, so Seek stays correct.

Code

Read for Take only updates limit:

        self.limit -= n as u64;

set_limit is documented as constructing a new Take and writes both fields:

    pub fn set_limit(&mut self, limit: u64) {
        self.len = limit;
        self.limit = limit;
    }

Linux specialization still uses set_limit as "subtract remaining":

impl<T: CopyRead> CopyRead for Take<T> {
    fn drain_to<W: Write>(&mut self, writer: &mut W, outer_limit: u64) -> Result<u64> {
        // ...
        self.set_limit(local_limit - bytes_drained);
        // ...
    }
    fn taken(&mut self, bytes: u64) {
        self.set_limit(self.limit() - bytes);
        self.get_mut().taken(bytes);
    }

Seek::stream_position is len - limit. After a full copy, set_limit(0) makes len == 0 and limit == 0, so position is 0. seek(Start(0)) then thinks it is already at the start.

Even a 0-byte drain_to calls set_limit(local_limit) and wipes len.

Fix

Decrement only limit (same as Read for Take). Do not call set_limit from taken / drain_to.

Origin

Lines Commit Date Author
kernel_copy Take uses set_limit to decrement remaining 7f5d2722afea 2020-11-13 The8472
taken also uses set_limit a9b1381b8dd0 2020-12-03 The8472
set_limit also resets len (Seek for Take) c8f5ff867d2 2025-05-19 Mario Pastorelli

set_limit only changed limit when the specialization was written. After Take gained len for Seek (stable 1.89), those set_limit calls started destroying the original length.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-ioI/O, stdio, buffered readersI-wrongWrong result or data corruptionO-linuxLinux-specificP-highHigh impact: affects correctness on common pathsbugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions