{"article":{"slug":"fixing-kio-copy-to-usb-stick-and-network-filesystems","title":"Fixing KIO copy to USB stick and network filesystems","subtitle":null,"summary":"KDE developer Méven Car tells the story of fixing a 15-year-old high-priority KIO bug where copies to USB sticks and network shares reported progress against the page cache rather than the device, using sync_file_range() so Dolphin's copy now finishes when the data is really on the stick and network progress tracks the server.","content_type":"blog_post","language":"en","canonical_url":"https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/","author":{"name":"Méven Car","url":null,"person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"KDE Blogs","url":"https://blogs.kde.org/","listing_slug":null,"listing":null},"topics":[{"name":"Linux","slug":"linux","url":"https://listedarticles.com/topics/linux"},{"name":"Open Source","slug":"open-source","url":"https://listedarticles.com/topics/open-source"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":3190,"reading_minutes":14,"published_at":"2026-10-27T00:00:01.000Z","added_at":"2026-10-28T11:11:31.230Z","updated_at":"2026-10-28T11:11:31.230Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/fixing-kio-copy-to-usb-stick-and-network-filesystems","markdown_url":"https://listedarticles.com/articles/fixing-kio-copy-to-usb-stick-and-network-filesystems.md","example":false,"citation":"Méven Car, KDE Blogs. \"Fixing KIO copy to USB stick and network filesystems.\" 27 Oct 2026. https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/ (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/"},"body_markdown":"*Méven Car · KDE Blogs · October 27, 2026*\n\nI am going to tell the story of fixing a very high priority bug that was 15 years old. Arguably the worst KIO bug.\n\nIn September 2011, against KDE 4.7, someone opened [bug\n281270](https://bugs.kde.org/show_bug.cgi?id=281270): \"When copying files to a removable disk,\nspeed and progress reflect copying to the local cache not to the disk itself\". The report notes\nthe copy notification showed about 50 MB/s to a USB 1.1 device, which cannot go faster than\n12 Mbit/s. The reporter had asked a kernel developer, Alan Stern, what was going on, and summed up\nhis answer:\n\n> it shows that the copying operation is finished when it has not even been started.\n\nSeventy-seven comments and 17 duplicates later, that is still how a copy to a USB stick behaves in the\ncurrent release of KIO. My [last post\nabout copying](https://blogs.kde.org/2026/07/28/making-kio-copy-many-files-fast/) was about making KIO copy faster.\nThis one is about the opposite case, a destination that is slower than KIO, and about making the\nprogress bar and copy completion accurate.\n\nThe changes are in four KIO merge requests:\n[MR 2506](https://invent.kde.org/frameworks/kio/-/merge_requests/2506) finds out which devices are\nremovable, [MR 2517](https://invent.kde.org/frameworks/kio/-/merge_requests/2517) and\n[MR 2518](https://invent.kde.org/frameworks/kio/-/merge_requests/2518) flush the device at the end of\na copy or a move, and [MR 2283](https://invent.kde.org/frameworks/kio/-/merge_requests/2283) adds the\nwriteback window. All of it is merged, and will be in KDE Frameworks 6.32, released [November 13th 2026](https://community.kde.org/Schedules/Frameworks).\n\nWe will see the cause of the bug, its history, and what it took to fix it.\n\n## Why a copy finishes before it has started\n\nThe file worker copies a file by handing its data to `write()`. `write()` returns as soon as the\ndata is in the page cache, in memory. The kernel writes it to the device later, when it decides to.\nOn a local SSD, later means very soon, and you never notice. On a USB 2.0 stick that writes at\n2.3 MB/s, it is another story. Here is a 128 MiB file:\n\n[![A 128 MiB copy to a USB 2.0 stick before the change: the copy reports complete after 0.06 to 0.13 s, and the data is on the stick after 68 to 91 s, over four runs.](https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/usb-before.svg)](https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/usb-before.svg)\n\nFor more than a minute, Dolphin considers the copy finished while the stick is still writing. The\nprogress bar was showing how fast KIO filled memory. Pull the stick out during that minute, and your\ndata is gone.\n\nNetwork filesystems have a different symptom from the same cause. A large copy to an NFS or SMB\nshare leaves hundreds of MiB in memory to send to the server, and the progress runs far ahead of\nit. On SMB it is worse than a wrong number: with the default `cache=strict`, looking at a file\nwaits until its dirty data is written. Listing the folder you are copying into looks at that file.\nOver a 5 MB/s link, almost every listing during a 1 GiB copy waited 15 to 20 seconds.\n\n## Fifteen years in one bug report\n\nThe right tool actually showed up very early. Two days after the report, Christoph Feck asked whether there was a\nway to learn when the data is really on the disk \"without causing massive slow downs that any sync\ntype calls would cause\". Two weeks later, the reporter passed on Alan Stern's answer,\n`sync_file_range()`, and Christoph replied that it looked like \"exactly what we need\". That was\nSeptember 2011, and it is the call KIO now uses.\n\nThen not much happened for years, apart from users confirming it and seventeen duplicates. In 2021, Nate\nGraham raised it to the highest priority, because a move deletes the source files while the copies\nare still in memory. In 2022, he changed the Disks & Devices applet, so that ejecting a device\nexplains that it is still writing. In 2024, Christoph tested the `cachestat()` call of Linux 6.5,\nwhich reports how much of a file is still dirty in memory, but only for one file at a time.\n\nThis bug has been my white whale. I have been contributing to KIO since 2018, and I am still learning\nhow the Linux storage stack behaves underneath it. I tried twice in 2024 and did not get\n`sync_file_range()` right. In [MR 1718](https://invent.kde.org/frameworks/kio/-/merge_requests/1718),\nthe `sync_file_range()` path stayed commented out. In [KIO\nMR 1764](https://invent.kde.org/frameworks/kio/-/merge_requests/1764) and [Dolphin\nMR 862](https://invent.kde.org/system/dolphin/-/merge_requests/862), the worker waited for the\nwriteback of the chunk it had just written, which had not started, so it never waited at all. Waiting\nfor the window before the one just written is what makes it work. I didn't spot the issue, nor any\nreviewer unfortunately.\n\nSince then, another contributor proposed turning off the cache for copies, in\n[MR 2162](https://invent.kde.org/frameworks/kio/-/merge_requests/2162), which was closed.\n\n## Why it took so long\n\nMaking the copy correct is the easy part: call `fsync()` after every file, or open the destination with\n`O_DIRECT` and skip the cache. Both make copies several times slower, and they would slow down\nthe fast local disks too, where nothing was wrong. Making it\ncorrect without making it slow is what took fifteen years, and it needed several things that were\nnot there before.\n\n- A way to wait for part of a file. POSIX only offers `fsync()`, which waits for everything.\n  `sync_file_range()` is Linux only, and its own manual calls it \"extremely dangerous\": it does not\n  write the file's metadata, so on its own it guarantees nothing after a crash. It can bound how\n  much is in memory, but the end of the copy still needs a real flush.\n- Knowing which destinations need it, without asking them. The filesystem type does not show\n  whether a device can be unplugged, and asking a network filesystem anything is a round trip to a\n  server that may be slow. KIO reads the answer from the\n  mount table and from sysfs instead, and two earlier changes to `KMountPoint` made that cheap\n  enough to do for every copied file:\n  - [MR 2326](https://invent.kde.org/frameworks/kio/-/merge_requests/2326), in KIO 6.30, caches\n    mounts by the mount id that `statx()` returns since Linux 6.8, instead of parsing\n    `/proc/self/mountinfo` on every lookup. A lookup by id became about 700 times faster.\n  - [MR 2487](https://invent.kde.org/frameworks/kio/-/merge_requests/2487), in KIO 6.32, reads a\n    missing mount with `statmount()`, which does not walk any path, so a network server that does\n    not answer no longer blocks the lookup. In that case, the lookup went from more than 15 seconds\n    to about 1 ms.\n- A place to flush at the end of a copy. The file worker copies one file per command, in its own\n  process, and never knows which file is the last one. Only the `CopyJob`, in the application, knows\n  when the whole copy is over. So the flush needed a new worker command, and a way for the worker to\n  notify the job that a flush is needed. On the way, I found that `FileCopyJob` dropped the metadata\n  that the worker sent with a copy, so the `CopyJob` could never have seen it.\n- A window size that fits every destination. A window too small slows a fast link, and a window\n  too large never fills on a slow one. That is why it now adapts, and why the progress has to stay\n  monotonic when the window shrinks.\n\n## A writeback window\n\nWaiting for the device after every write would be correct but very slow. Waiting only at the end\nleaves the progress wrong for the whole copy. KIO now does something in between, with\n`sync_file_range()`, which lets a program start the writeback of part of a file and wait for it.\n\nThe data is written in windows. When a window is full, the worker hands it to a helper thread,\nwhich writes it back and waits for it, while the copy fills the next window. The copy only waits\nwhen the window before the one it hands over is not written yet. So at most two windows of the copy\nare ever in memory and not on the device, and the progress bar is never more than one window ahead\nof what is really written.\n\nThe helper thread was needed to accommodate CIFS, the kernel client for Samba and Windows shares. On CIFS,\nthe call that starts the writeback of a window does more than start it: it sends the data to the\nserver, and waits for the server to grant the credits to do so, blocking the calling thread for about 157 ms per 128 MiB window.\nSo the copy could not fill the next window while the previous one was sent. With the helper thread making that call, sending a window and\nfilling the next one overlap.\n\nA window holds about 200 ms of writeback, at the speed measured on the window before. It starts at\n1 MiB, can at most double from one window to the next, and stays below 16 MiB, or 128 MiB on CIFS.\nOn a fast link the windows grow, so the copy keeps its speed. On a slow link they stay at about\n1 MiB, and anything that has to wait for the file waits a fraction of a second, not for the whole\nfile.\n\n## Complete means on the device\n\nA window bounds how much is left in memory, but there is still something left at the end. For a\nremovable device, KIO now also makes the end of the copy wait for it.\n\nA file larger than the largest window, 16 MiB, is flushed with `fsync()`, and then its folder, before\nthe copy reports it done. By then the window has already written all but the last two windows, so the flush is\nshort.\n\nSmall files are the hard part. The obvious fix, an `fsync()` for every file, made 3000 files of\n16 KiB take 10 seconds instead of 0.04 seconds on a local disk. So the worker flushes no small file\nby itself. After a copy to a removable device, it notifies the job, and the `CopyJob` sends it one\nmore command before reporting success: write back the whole filesystem of the destination, with\none `syncfs()`. While that runs, the copy notification shows \"Writing the copied data to the device...\".\nIf the flush fails, the copy fails, because the data may not be on the device.\n\nA move needed more than that. Before, KIO deleted each source file right after its copy, while\nthe copy could still be only in memory, which is why the bug got its highest priority. Now a move to\na removable device keeps its sources until the flush has succeeded, and deletes them after it. If the\nflush fails, or the stick is unplugged during it, every source stays. A source that changed while it\nwaited for the flush stays too, with a warning. A copy or a move that stops on an error, or that you\ncancel, still flushes the device, and a move then deletes only the sources of the files that are on\nit.\n\nHere is the same USB stick again, with the small files added:\n\n[![Copies to a USB 2.0 stick, before and after the change. 128 MiB file: the copy reports complete after 0.06 to 0.13 s before, over four runs, and 28.1 s after; the data is on the stick after 68 to 91 s before and 28.1 s after. 3000 files of 16 KiB: complete after 1.2 s before and 183.9 s after; on the stick after 185.2 s before and 183.9 s after. Progress ahead of the stick: 128 MiB before, 3 MiB after.](https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/usb.svg)](https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/usb.svg)\n\nYes, the copies now take longer by the clock, but only because the clock now includes the time the\nstick spends writing. The time until the data is really on the stick did not get worse. The speed of this stick\nvaries a lot from run to run, so the 28 seconds against 68 to 91 seconds for the large file is not a\nspeedup I would count on. The small files never fill a window, so they only depend on the flush at\nthe end.\n\n## Which destinations get a window\n\nA local fixed disk does not need any of this, and keeps the current behaviour exactly. With the\nremovable policy forced onto a local disk for testing, the 3000 small files took 1.55 s against\n1.54 s before the change.\n\nDeciding what is removable is less obvious than it sounds. The filesystem type does not expose it: exFAT\ncan be a memory card, and FAT is also the EFI partition of your laptop. Most USB disks do not even\nreport removable media. [MR 2506](https://invent.kde.org/frameworks/kio/-/merge_requests/2506) adds a private function to KIO that asks sysfs: the disk's own\nremovable flag, then the USB port or PCIe slot it hangs off, which the kernel marks removable when\nit can be reached from outside the machine, and SD cards. It follows encrypted and LVM volumes down\nto the disk under them.\n\nNetwork filesystems are easier: every mount that KIO already considers slow gets a window. That is\nNFS and CIFS, but also Ceph, GlusterFS, 9p, sshfs, and FUSE filesystems such as rclone. The worker\nfinds the mount from the mount id of the destination folder, without a round trip to the server.\n\n## Numbers on network shares\n\nTo measure this, I used KIO's own copy job, comparing KIO master just before the change with KIO\nmaster after it, while another thread listed the destination folder every 50 ms. The SMB server ran on the same machine, with 2 ms of\ndelay added to its traffic, and for the slow link a limit of 40 Mbit/s, about 5 MB/s.\n\nOn the slow link, the listing is the difference you would feel:\n\n[![Listing a folder during a copy of 256 MiB to an SMB share at 5 MB/s, median seconds. Round 1: 11.3 s before the change, 1.7 s after. Round 2: 5.5 s and 1.7 s.](https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/smb-listing.svg)](https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/smb-listing.svg)\n\n[![Copy of 256 MiB to an SMB share at 5 MB/s, 2 rounds. Time until complete: 54.2 and 54.4 s before, 55.9 and 55.9 s after, 4.95 MB/s against 4.80 MB/s. Progress ahead of the server: 256 MiB before, 1 MiB after. Slowest listing during the copy: 27.4 and 15.3 s before, 1.70 and 1.70 s after.](https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/smb-slow.svg)](https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/smb-slow.svg)\n\nThe copy runs about 3% slower with the window. On a fast link, the copy keeps up:\n\n[![Copy of 4 GiB to an SMB share with 2 ms of delay, 5 rounds. Median time until complete: 7.15 s before, 6.06 s after. Largest progress ahead of the server: 1334 MiB before, 128 MiB after. Listing during the copy: median 60.9 ms before and 59.6 ms after, slowest 731 ms before and 111 ms after.](https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/smb-fast.svg)](https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/smb-fast.svg)\n\nOne of the five copies before the change failed with a write error from the share, so these\nnumbers come from four rounds. The times vary between runs: in an earlier run, the copy with the\nwindow was 4% slower.\n\nGetting there took me four tries. My first version was 10 to 20% slower on the fast link, because\nof the CIFS behaviour described above. Then, with the windows aimed at 500 ms, listings on the slow\nlink went up to 4.2 seconds, because each request of a listing waits behind the window being sent.\nAiming the windows at 200 ms brought the listings down to 1.7 seconds.\n\nNFS, on the same machine with 2 ms of delay:\n\n[![Copy of 4 GiB to an NFS share with 2 ms of delay, medians of 3 rounds. Time until complete: 9.8 s before, 10.4 s after. Progress ahead of the server: 14 to 23 MiB before, 1 to 5 MiB after. Listing during the copy: median 24 ms before and 11 ms after, 95th percentile 63 ms before and 40 ms after.](https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/nfs.svg)](https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/nfs.svg)\n\nThe copy times are close: 11% slower in the first round, and slightly faster in the two others.\n\n## Part of a frozen Dolphin\n\n[Bug 423187](https://bugs.kde.org/show_bug.cgi?id=423187) collects many ways in which a slow mount\ncan freeze Dolphin. In one of its comments, Frank Gehann copied a 100 MB file to an SMB share while\nbrowsing it, and found two causes: Dolphin's main thread waiting for answers from the share, and\nfolder listings waiting behind the queued upload.\n\nThe window helps with the second cause: each request of a listing now waits behind at most two\nwindows, about 400 ms of writeback, and a whole listing took 1.7 seconds on the 5 MB/s link instead\nof 5 to 11. The [`statmount()` change](https://invent.kde.org/frameworks/kio/-/merge_requests/2487)\nhelps with part of the first one. `KFileItem::isSlow()`, which Dolphin calls on its main thread\nbefore it reads a file to find its type, no longer waits for any server, on Linux 6.8 or later. Work is underway to fix the\nrest of the first cause in Frank's open\n[MR 2484](https://invent.kde.org/frameworks/kio/-/merge_requests/2484) and\n[MR 2485](https://invent.kde.org/frameworks/kio/-/merge_requests/2485).\n\n## The stick that pauses\n\nOne measurement sent me down a rabbit hole. During a large copy to the USB stick, listing a folder on\nthe stick sometimes took about 15 seconds, before the change as much as after it. It looked like the\nwriteback window was making something wait.\n\nIt was the stick. Roughly every 8 seconds, a single 120 KiB write to it took 4 to 7 seconds to\ncomplete, and the listing waited for a page that was stuck behind such a write. KIO cannot fix a\ndevice that stops for seconds at a time, but at least we now know the copy is not to blame. In the runs for this post, the\nslowest listing took 3.6 ms during the copy of the large file, but 7 seconds during the copy of the\nsmall files, against 15 seconds before the change.\n\n## Copying between two filesystems\n\nOne more thing landed along the way, and this one is a plain speedup. In 2021, in Frameworks 5.88, I replaced `sendfile()` with\n`copy_file_range()` in the file worker, in\n[MR 602](https://invent.kde.org/frameworks/kio/-/merge_requests/602). At the time, Linux copied with `copy_file_range()` across\nfilesystems too. Linux 5.19 changed that back: between two filesystems, the call fails again with\n`EXDEV`. Since then, a copy from one filesystem to another went through `read()` and `write()` in the\nworker, one buffer at a time.\n\n[MR 2520](https://invent.kde.org/frameworks/kio/-/merge_requests/2520) now falls back to\n`sendfile()` in that case, which copies from one file to another inside the kernel:\n\n[![Copy of 1 GiB between two filesystems, medians of 5 rounds. tmpfs to ext4: 443 ms before, 272 ms after. ext4 to tmpfs: 531 ms before, 278 ms after.](https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/sendfile.svg)](https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/sendfile.svg)\n\nA copy to the USB stick takes as long as before, because its write\nspeed is the limit. This one is merged too, and will also be in KDE Frameworks 6.32.\n\n## Closing\n\nWith all this, a copy to a USB stick now finishes when the data is actually on the stick.\nOn network filesystems, the progress now follows what the server has received, and the share stays\nusable during the copy.\n\n`sync_file_range()` is mainly used by databases and storage engines. No other file manager that I\nknow of uses it. Now KIO, and so Dolphin, uses it too.\n\nHappy unplugging.\n\n---\n\n*Originally published at [KDE Blogs](https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/).*\n","body_html":"<p><em>Méven Car · KDE Blogs · October 27, 2026</em></p>\n<p>I am going to tell the story of fixing a very high priority bug that was 15 years old. Arguably the worst KIO bug.</p>\n<p>In September 2011, against KDE 4.7, someone opened <a href=\"https://bugs.kde.org/show_bug.cgi?id=281270\" rel=\"nofollow ugc noopener\">bug\n281270</a>: &quot;When copying files to a removable disk,\nspeed and progress reflect copying to the local cache not to the disk itself&quot;. The report notes\nthe copy notification showed about 50 MB/s to a USB 1.1 device, which cannot go faster than\n12 Mbit/s. The reporter had asked a kernel developer, Alan Stern, what was going on, and summed up\nhis answer:</p>\n<blockquote><p>it shows that the copying operation is finished when it has not even been started.</p></blockquote>\n<p>Seventy-seven comments and 17 duplicates later, that is still how a copy to a USB stick behaves in the\ncurrent release of KIO. My <a href=\"https://blogs.kde.org/2026/07/28/making-kio-copy-many-files-fast/\" rel=\"nofollow ugc noopener\">last post\nabout copying</a> was about making KIO copy faster.\nThis one is about the opposite case, a destination that is slower than KIO, and about making the\nprogress bar and copy completion accurate.</p>\n<p>The changes are in four KIO merge requests:\n<a href=\"https://invent.kde.org/frameworks/kio/-/merge_requests/2506\" rel=\"nofollow ugc noopener\">MR 2506</a> finds out which devices are\nremovable, <a href=\"https://invent.kde.org/frameworks/kio/-/merge_requests/2517\" rel=\"nofollow ugc noopener\">MR 2517</a> and\n<a href=\"https://invent.kde.org/frameworks/kio/-/merge_requests/2518\" rel=\"nofollow ugc noopener\">MR 2518</a> flush the device at the end of\na copy or a move, and <a href=\"https://invent.kde.org/frameworks/kio/-/merge_requests/2283\" rel=\"nofollow ugc noopener\">MR 2283</a> adds the\nwriteback window. All of it is merged, and will be in KDE Frameworks 6.32, released <a href=\"https://community.kde.org/Schedules/Frameworks\" rel=\"nofollow ugc noopener\">November 13th 2026</a>.</p>\n<p>We will see the cause of the bug, its history, and what it took to fix it.</p>\n<h2 id=\"why-a-copy-finishes-before-it-has-started\">Why a copy finishes before it has started</h2>\n<p>The file worker copies a file by handing its data to <code>write()</code>. <code>write()</code> returns as soon as the\ndata is in the page cache, in memory. The kernel writes it to the device later, when it decides to.\nOn a local SSD, later means very soon, and you never notice. On a USB 2.0 stick that writes at\n2.3 MB/s, it is another story. Here is a 128 MiB file:</p>\n<p><a href=\"https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/usb-before.svg\" rel=\"nofollow ugc noopener\"><img src=\"https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/usb-before.svg\" alt=\"A 128 MiB copy to a USB 2.0 stick before the change: the copy reports complete after 0.06 to 0.13 s, and the data is on the stick after 68 to 91 s, over four runs.\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></a></p>\n<p>For more than a minute, Dolphin considers the copy finished while the stick is still writing. The\nprogress bar was showing how fast KIO filled memory. Pull the stick out during that minute, and your\ndata is gone.</p>\n<p>Network filesystems have a different symptom from the same cause. A large copy to an NFS or SMB\nshare leaves hundreds of MiB in memory to send to the server, and the progress runs far ahead of\nit. On SMB it is worse than a wrong number: with the default <code>cache=strict</code>, looking at a file\nwaits until its dirty data is written. Listing the folder you are copying into looks at that file.\nOver a 5 MB/s link, almost every listing during a 1 GiB copy waited 15 to 20 seconds.</p>\n<h2 id=\"fifteen-years-in-one-bug-report\">Fifteen years in one bug report</h2>\n<p>The right tool actually showed up very early. Two days after the report, Christoph Feck asked whether there was a\nway to learn when the data is really on the disk &quot;without causing massive slow downs that any sync\ntype calls would cause&quot;. Two weeks later, the reporter passed on Alan Stern&#39;s answer,\n<code>sync_file_range()</code>, and Christoph replied that it looked like &quot;exactly what we need&quot;. That was\nSeptember 2011, and it is the call KIO now uses.</p>\n<p>Then not much happened for years, apart from users confirming it and seventeen duplicates. In 2021, Nate\nGraham raised it to the highest priority, because a move deletes the source files while the copies\nare still in memory. In 2022, he changed the Disks &amp; Devices applet, so that ejecting a device\nexplains that it is still writing. In 2024, Christoph tested the <code>cachestat()</code> call of Linux 6.5,\nwhich reports how much of a file is still dirty in memory, but only for one file at a time.</p>\n<p>This bug has been my white whale. I have been contributing to KIO since 2018, and I am still learning\nhow the Linux storage stack behaves underneath it. I tried twice in 2024 and did not get\n<code>sync_file_range()</code> right. In <a href=\"https://invent.kde.org/frameworks/kio/-/merge_requests/1718\" rel=\"nofollow ugc noopener\">MR 1718</a>,\nthe <code>sync_file_range()</code> path stayed commented out. In <a href=\"https://invent.kde.org/frameworks/kio/-/merge_requests/1764\" rel=\"nofollow ugc noopener\">KIO\nMR 1764</a> and <a href=\"https://invent.kde.org/system/dolphin/-/merge_requests/862\" rel=\"nofollow ugc noopener\">Dolphin\nMR 862</a>, the worker waited for the\nwriteback of the chunk it had just written, which had not started, so it never waited at all. Waiting\nfor the window before the one just written is what makes it work. I didn&#39;t spot the issue, nor any\nreviewer unfortunately.</p>\n<p>Since then, another contributor proposed turning off the cache for copies, in\n<a href=\"https://invent.kde.org/frameworks/kio/-/merge_requests/2162\" rel=\"nofollow ugc noopener\">MR 2162</a>, which was closed.</p>\n<h2 id=\"why-it-took-so-long\">Why it took so long</h2>\n<p>Making the copy correct is the easy part: call <code>fsync()</code> after every file, or open the destination with\n<code>O_DIRECT</code> and skip the cache. Both make copies several times slower, and they would slow down\nthe fast local disks too, where nothing was wrong. Making it\ncorrect without making it slow is what took fifteen years, and it needed several things that were\nnot there before.</p>\n<ul><li><p>A way to wait for part of a file. POSIX only offers <code>fsync()</code>, which waits for everything.</p><p><code>sync_file_range()</code> is Linux only, and its own manual calls it &quot;extremely dangerous&quot;: it does not\nwrite the file&#39;s metadata, so on its own it guarantees nothing after a crash. It can bound how\nmuch is in memory, but the end of the copy still needs a real flush.</p></li><li><p>Knowing which destinations need it, without asking them. The filesystem type does not show</p><p>whether a device can be unplugged, and asking a network filesystem anything is a round trip to a\nserver that may be slow. KIO reads the answer from the\nmount table and from sysfs instead, and two earlier changes to <code>KMountPoint</code> made that cheap\nenough to do for every copied file:</p>\n<ul><li><p><a href=\"https://invent.kde.org/frameworks/kio/-/merge_requests/2326\" rel=\"nofollow ugc noopener\">MR 2326</a>, in KIO 6.30, caches</p><p>mounts by the mount id that <code>statx()</code> returns since Linux 6.8, instead of parsing\n<code>/proc/self/mountinfo</code> on every lookup. A lookup by id became about 700 times faster.</p></li><li><p><a href=\"https://invent.kde.org/frameworks/kio/-/merge_requests/2487\" rel=\"nofollow ugc noopener\">MR 2487</a>, in KIO 6.32, reads a</p><p>missing mount with <code>statmount()</code>, which does not walk any path, so a network server that does\nnot answer no longer blocks the lookup. In that case, the lookup went from more than 15 seconds\nto about 1 ms.</p></li></ul></li><li><p>A place to flush at the end of a copy. The file worker copies one file per command, in its own</p><p>process, and never knows which file is the last one. Only the <code>CopyJob</code>, in the application, knows\nwhen the whole copy is over. So the flush needed a new worker command, and a way for the worker to\nnotify the job that a flush is needed. On the way, I found that <code>FileCopyJob</code> dropped the metadata\nthat the worker sent with a copy, so the <code>CopyJob</code> could never have seen it.</p></li><li><p>A window size that fits every destination. A window too small slows a fast link, and a window</p><p>too large never fills on a slow one. That is why it now adapts, and why the progress has to stay\nmonotonic when the window shrinks.</p></li></ul>\n<h2 id=\"a-writeback-window\">A writeback window</h2>\n<p>Waiting for the device after every write would be correct but very slow. Waiting only at the end\nleaves the progress wrong for the whole copy. KIO now does something in between, with\n<code>sync_file_range()</code>, which lets a program start the writeback of part of a file and wait for it.</p>\n<p>The data is written in windows. When a window is full, the worker hands it to a helper thread,\nwhich writes it back and waits for it, while the copy fills the next window. The copy only waits\nwhen the window before the one it hands over is not written yet. So at most two windows of the copy\nare ever in memory and not on the device, and the progress bar is never more than one window ahead\nof what is really written.</p>\n<p>The helper thread was needed to accommodate CIFS, the kernel client for Samba and Windows shares. On CIFS,\nthe call that starts the writeback of a window does more than start it: it sends the data to the\nserver, and waits for the server to grant the credits to do so, blocking the calling thread for about 157 ms per 128 MiB window.\nSo the copy could not fill the next window while the previous one was sent. With the helper thread making that call, sending a window and\nfilling the next one overlap.</p>\n<p>A window holds about 200 ms of writeback, at the speed measured on the window before. It starts at\n1 MiB, can at most double from one window to the next, and stays below 16 MiB, or 128 MiB on CIFS.\nOn a fast link the windows grow, so the copy keeps its speed. On a slow link they stay at about\n1 MiB, and anything that has to wait for the file waits a fraction of a second, not for the whole\nfile.</p>\n<h2 id=\"complete-means-on-the-device\">Complete means on the device</h2>\n<p>A window bounds how much is left in memory, but there is still something left at the end. For a\nremovable device, KIO now also makes the end of the copy wait for it.</p>\n<p>A file larger than the largest window, 16 MiB, is flushed with <code>fsync()</code>, and then its folder, before\nthe copy reports it done. By then the window has already written all but the last two windows, so the flush is\nshort.</p>\n<p>Small files are the hard part. The obvious fix, an <code>fsync()</code> for every file, made 3000 files of\n16 KiB take 10 seconds instead of 0.04 seconds on a local disk. So the worker flushes no small file\nby itself. After a copy to a removable device, it notifies the job, and the <code>CopyJob</code> sends it one\nmore command before reporting success: write back the whole filesystem of the destination, with\none <code>syncfs()</code>. While that runs, the copy notification shows &quot;Writing the copied data to the device...&quot;.\nIf the flush fails, the copy fails, because the data may not be on the device.</p>\n<p>A move needed more than that. Before, KIO deleted each source file right after its copy, while\nthe copy could still be only in memory, which is why the bug got its highest priority. Now a move to\na removable device keeps its sources until the flush has succeeded, and deletes them after it. If the\nflush fails, or the stick is unplugged during it, every source stays. A source that changed while it\nwaited for the flush stays too, with a warning. A copy or a move that stops on an error, or that you\ncancel, still flushes the device, and a move then deletes only the sources of the files that are on\nit.</p>\n<p>Here is the same USB stick again, with the small files added:</p>\n<p><a href=\"https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/usb.svg\" rel=\"nofollow ugc noopener\"><img src=\"https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/usb.svg\" alt=\"Copies to a USB 2.0 stick, before and after the change. 128 MiB file: the copy reports complete after 0.06 to 0.13 s before, over four runs, and 28.1 s after; the data is on the stick after 68 to 91 s before and 28.1 s after. 3000 files of 16 KiB: complete after 1.2 s before and 183.9 s after; on the stick after 185.2 s before and 183.9 s after. Progress ahead of the stick: 128 MiB before, 3 MiB after.\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></a></p>\n<p>Yes, the copies now take longer by the clock, but only because the clock now includes the time the\nstick spends writing. The time until the data is really on the stick did not get worse. The speed of this stick\nvaries a lot from run to run, so the 28 seconds against 68 to 91 seconds for the large file is not a\nspeedup I would count on. The small files never fill a window, so they only depend on the flush at\nthe end.</p>\n<h2 id=\"which-destinations-get-a-window\">Which destinations get a window</h2>\n<p>A local fixed disk does not need any of this, and keeps the current behaviour exactly. With the\nremovable policy forced onto a local disk for testing, the 3000 small files took 1.55 s against\n1.54 s before the change.</p>\n<p>Deciding what is removable is less obvious than it sounds. The filesystem type does not expose it: exFAT\ncan be a memory card, and FAT is also the EFI partition of your laptop. Most USB disks do not even\nreport removable media. <a href=\"https://invent.kde.org/frameworks/kio/-/merge_requests/2506\" rel=\"nofollow ugc noopener\">MR 2506</a> adds a private function to KIO that asks sysfs: the disk&#39;s own\nremovable flag, then the USB port or PCIe slot it hangs off, which the kernel marks removable when\nit can be reached from outside the machine, and SD cards. It follows encrypted and LVM volumes down\nto the disk under them.</p>\n<p>Network filesystems are easier: every mount that KIO already considers slow gets a window. That is\nNFS and CIFS, but also Ceph, GlusterFS, 9p, sshfs, and FUSE filesystems such as rclone. The worker\nfinds the mount from the mount id of the destination folder, without a round trip to the server.</p>\n<h2 id=\"numbers-on-network-shares\">Numbers on network shares</h2>\n<p>To measure this, I used KIO&#39;s own copy job, comparing KIO master just before the change with KIO\nmaster after it, while another thread listed the destination folder every 50 ms. The SMB server ran on the same machine, with 2 ms of\ndelay added to its traffic, and for the slow link a limit of 40 Mbit/s, about 5 MB/s.</p>\n<p>On the slow link, the listing is the difference you would feel:</p>\n<p><a href=\"https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/smb-listing.svg\" rel=\"nofollow ugc noopener\"><img src=\"https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/smb-listing.svg\" alt=\"Listing a folder during a copy of 256 MiB to an SMB share at 5 MB/s, median seconds. Round 1: 11.3 s before the change, 1.7 s after. Round 2: 5.5 s and 1.7 s.\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></a></p>\n<p><a href=\"https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/smb-slow.svg\" rel=\"nofollow ugc noopener\"><img src=\"https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/smb-slow.svg\" alt=\"Copy of 256 MiB to an SMB share at 5 MB/s, 2 rounds. Time until complete: 54.2 and 54.4 s before, 55.9 and 55.9 s after, 4.95 MB/s against 4.80 MB/s. Progress ahead of the server: 256 MiB before, 1 MiB after. Slowest listing during the copy: 27.4 and 15.3 s before, 1.70 and 1.70 s after.\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></a></p>\n<p>The copy runs about 3% slower with the window. On a fast link, the copy keeps up:</p>\n<p><a href=\"https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/smb-fast.svg\" rel=\"nofollow ugc noopener\"><img src=\"https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/smb-fast.svg\" alt=\"Copy of 4 GiB to an SMB share with 2 ms of delay, 5 rounds. Median time until complete: 7.15 s before, 6.06 s after. Largest progress ahead of the server: 1334 MiB before, 128 MiB after. Listing during the copy: median 60.9 ms before and 59.6 ms after, slowest 731 ms before and 111 ms after.\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></a></p>\n<p>One of the five copies before the change failed with a write error from the share, so these\nnumbers come from four rounds. The times vary between runs: in an earlier run, the copy with the\nwindow was 4% slower.</p>\n<p>Getting there took me four tries. My first version was 10 to 20% slower on the fast link, because\nof the CIFS behaviour described above. Then, with the windows aimed at 500 ms, listings on the slow\nlink went up to 4.2 seconds, because each request of a listing waits behind the window being sent.\nAiming the windows at 200 ms brought the listings down to 1.7 seconds.</p>\n<p>NFS, on the same machine with 2 ms of delay:</p>\n<p><a href=\"https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/nfs.svg\" rel=\"nofollow ugc noopener\"><img src=\"https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/nfs.svg\" alt=\"Copy of 4 GiB to an NFS share with 2 ms of delay, medians of 3 rounds. Time until complete: 9.8 s before, 10.4 s after. Progress ahead of the server: 14 to 23 MiB before, 1 to 5 MiB after. Listing during the copy: median 24 ms before and 11 ms after, 95th percentile 63 ms before and 40 ms after.\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></a></p>\n<p>The copy times are close: 11% slower in the first round, and slightly faster in the two others.</p>\n<h2 id=\"part-of-a-frozen-dolphin\">Part of a frozen Dolphin</h2>\n<p><a href=\"https://bugs.kde.org/show_bug.cgi?id=423187\" rel=\"nofollow ugc noopener\">Bug 423187</a> collects many ways in which a slow mount\ncan freeze Dolphin. In one of its comments, Frank Gehann copied a 100 MB file to an SMB share while\nbrowsing it, and found two causes: Dolphin&#39;s main thread waiting for answers from the share, and\nfolder listings waiting behind the queued upload.</p>\n<p>The window helps with the second cause: each request of a listing now waits behind at most two\nwindows, about 400 ms of writeback, and a whole listing took 1.7 seconds on the 5 MB/s link instead\nof 5 to 11. The <a href=\"https://invent.kde.org/frameworks/kio/-/merge_requests/2487\" rel=\"nofollow ugc noopener\"><code>statmount()</code> change</a>\nhelps with part of the first one. <code>KFileItem::isSlow()</code>, which Dolphin calls on its main thread\nbefore it reads a file to find its type, no longer waits for any server, on Linux 6.8 or later. Work is underway to fix the\nrest of the first cause in Frank&#39;s open\n<a href=\"https://invent.kde.org/frameworks/kio/-/merge_requests/2484\" rel=\"nofollow ugc noopener\">MR 2484</a> and\n<a href=\"https://invent.kde.org/frameworks/kio/-/merge_requests/2485\" rel=\"nofollow ugc noopener\">MR 2485</a>.</p>\n<h2 id=\"the-stick-that-pauses\">The stick that pauses</h2>\n<p>One measurement sent me down a rabbit hole. During a large copy to the USB stick, listing a folder on\nthe stick sometimes took about 15 seconds, before the change as much as after it. It looked like the\nwriteback window was making something wait.</p>\n<p>It was the stick. Roughly every 8 seconds, a single 120 KiB write to it took 4 to 7 seconds to\ncomplete, and the listing waited for a page that was stuck behind such a write. KIO cannot fix a\ndevice that stops for seconds at a time, but at least we now know the copy is not to blame. In the runs for this post, the\nslowest listing took 3.6 ms during the copy of the large file, but 7 seconds during the copy of the\nsmall files, against 15 seconds before the change.</p>\n<h2 id=\"copying-between-two-filesystems\">Copying between two filesystems</h2>\n<p>One more thing landed along the way, and this one is a plain speedup. In 2021, in Frameworks 5.88, I replaced <code>sendfile()</code> with\n<code>copy_file_range()</code> in the file worker, in\n<a href=\"https://invent.kde.org/frameworks/kio/-/merge_requests/602\" rel=\"nofollow ugc noopener\">MR 602</a>. At the time, Linux copied with <code>copy_file_range()</code> across\nfilesystems too. Linux 5.19 changed that back: between two filesystems, the call fails again with\n<code>EXDEV</code>. Since then, a copy from one filesystem to another went through <code>read()</code> and <code>write()</code> in the\nworker, one buffer at a time.</p>\n<p><a href=\"https://invent.kde.org/frameworks/kio/-/merge_requests/2520\" rel=\"nofollow ugc noopener\">MR 2520</a> now falls back to\n<code>sendfile()</code> in that case, which copies from one file to another inside the kernel:</p>\n<p><a href=\"https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/sendfile.svg\" rel=\"nofollow ugc noopener\"><img src=\"https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/sendfile.svg\" alt=\"Copy of 1 GiB between two filesystems, medians of 5 rounds. tmpfs to ext4: 443 ms before, 272 ms after. ext4 to tmpfs: 531 ms before, 278 ms after.\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></a></p>\n<p>A copy to the USB stick takes as long as before, because its write\nspeed is the limit. This one is merged too, and will also be in KDE Frameworks 6.32.</p>\n<h2 id=\"closing\">Closing</h2>\n<p>With all this, a copy to a USB stick now finishes when the data is actually on the stick.\nOn network filesystems, the progress now follows what the server has received, and the share stays\nusable during the copy.</p>\n<p><code>sync_file_range()</code> is mainly used by databases and storage engines. No other file manager that I\nknow of uses it. Now KIO, and so Dolphin, uses it too.</p>\n<p>Happy unplugging.</p>\n<hr />\n<p><em>Originally published at <a href=\"https://blogs.kde.org/2026/10/27/fixing-kio-copy-to-usb-stick-and-network-filesystems/\" rel=\"nofollow ugc noopener\">KDE Blogs</a>.</em></p>","headings":[{"level":2,"text":"Why a copy finishes before it has started","id":"why-a-copy-finishes-before-it-has-started"},{"level":2,"text":"Fifteen years in one bug report","id":"fifteen-years-in-one-bug-report"},{"level":2,"text":"Why it took so long","id":"why-it-took-so-long"},{"level":2,"text":"A writeback window","id":"a-writeback-window"},{"level":2,"text":"Complete means on the device","id":"complete-means-on-the-device"},{"level":2,"text":"Which destinations get a window","id":"which-destinations-get-a-window"},{"level":2,"text":"Numbers on network shares","id":"numbers-on-network-shares"},{"level":2,"text":"Part of a frozen Dolphin","id":"part-of-a-frozen-dolphin"},{"level":2,"text":"The stick that pauses","id":"the-stick-that-pauses"},{"level":2,"text":"Copying between two filesystems","id":"copying-between-two-filesystems"},{"level":2,"text":"Closing","id":"closing"}]}}