Fixing "Text file busy"
What does "Text file busy" mean?
Conclusion: You are trying to overwrite or truncate the file backing a program that is currently running (or a shared library in use). The kernel returns
ETXTBSY(errno 26). Either stop the running process, or switch from "overwrite" to "replace" (rename) and the error goes away.
It usually shows up while copying, building, or deploying a binary:
cp: cannot create regular file '/usr/local/bin/myapp': Text file busy
The "text" in Text file busy is the old Unix term for a program's text segment (its executable code). It has nothing to do with whether the file contains human-readable text - it means "the file holding executable code is in use."
The kernel returns this error mainly for these operations:
- Opening a running binary for writing (overwriting with
cp, redirecting with> file) - Overwriting a shared library (
.so) that is in use (already mmap'd into memory) - Trying to
execve()a file that is still open for writing (a build race)
Text file busy is not the same as Permission denied or Read-only file system. Permissions and the filesystem can be perfectly fine - the write is rejected solely because the file is being executed. Misreading the error leads to the wrong fix.
Why can't a running binary be overwritten?
Conclusion: Linux protects the inode of a running executable (and any loaded shared library) by denying writes. Modifying running code mid-flight would cause crashes or undefined behavior, so the kernel blocks it up front with
ETXTBSY.
When a program starts, the kernel mmaps the executable into memory and runs it. Pages are loaded lazily, so the file on disk keeps being referenced for the entire lifetime of the process.
If someone rewrites the file now, a not-yet-loaded page could change into something else and break consistency. To prevent this, the kernel marks the inode as "being executed" and rejects write-opens (O_WRONLY / O_RDWR) and truncation with ETXTBSY. Shared libraries contain executable code too, so they get the same protection.
The protection works in both directions. If you open a file for writing and then try to execve() that same file, you also get ETXTBSY. A build script that forgets to close its output file before running it hits exactly this case.
Note that shell scripts usually do not trigger this error. A script is just read as data by an interpreter like bash; it never becomes a protected executable image. ETXTBSY mainly affects ELF binaries and shared libraries.
How do I find the process holding it?
Conclusion: Use
lsof <file>orfuser <file>to list the process running that binary. The PID it prints is the one you need to stop. If it is managed by a service, stop it withsystemctlrather than killing it directly.
Inspect per file with lsof
Pass the path you want to overwrite to lsof, and it shows which process has it open or running.
lsof /usr/local/bin/myapp
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME myapp 4821 deploy txt REG 259,1 6291456 131080 /usr/local/bin/myapp
The txt in the FD column means "in use as executable text (code)." That myapp (PID 4821) is the culprit. For a shared library it appears as mem (mmap'd).
Get the PID fast with fuser
If you just want the PID quickly, use fuser.
fuser /usr/local/bin/myapp
/usr/local/bin/myapp: 4821e
The trailing e means executable being run. Add fuser -v to also see USER / COMMAND.
If the holder is a long-running service or daemon, killing it directly with kill can trigger an auto-restart or leave it in a half-updated state. Stop services with systemctl stop and only consider kill for manually started processes.
How do I replace it safely instead of overwriting?
Conclusion: If you cannot stop the running process, stop using
cpto overwrite and usemv(rename) orinstallto swap in a new file. Rename creates a fresh inode and re-points the directory entry, so the running process keeps its old inode and never collides with the new binary.
Why mv works but cp fails
cp new /usr/local/bin/myapp- opens the existing inode withO_TRUNCand rewrites it in place ->ETXTBSYbecause it is runningmv new /usr/local/bin/myapp- renames a new inode tomyappand replaces the old name -> never touches the running inode, so it succeeds
The key is to keep it within the same filesystem. Running mv from /tmp (a different filesystem) turns into copy-plus-delete internally and may take the overwrite path. The reliable pattern is to place the new file in the same directory, then rename.
# Download/build into the same directory, then rename cp myapp.new /usr/local/bin/myapp.new # stage under a temp name first mv /usr/local/bin/myapp.new /usr/local/bin/myapp # atomic swap
The running process keeps executing the old binary (gone from the directory, but its inode is still alive) and picks up the new one on the next start.
install is even cleaner
install does "write to a temp file, then rename" internally and sets permissions and ownership in one go - ideal for deployment.
sudo install -m 0755 -o root -g root myapp.new /usr/local/bin/myapp
If you must write to the same inode, remove it first
Delete the directory entry with rm, then write - it becomes a fresh create and avoids ETXTBSY (unlink itself is allowed even while running).
sudo rm /usr/local/bin/myapp # unlink works even while it runs sudo cp myapp.new /usr/local/bin/myapp
The most reliable option is "stop the process, then overwrite": for a service, systemctl stop myapp && cp ... && systemctl start myapp. Use the rename/install approach when you need to swap it in without downtime - that split makes the decision quick.
How do I keep it from recurring in deploys and builds?
Conclusion: Make deployments "atomic replace via rename," not "overwrite." In builds, reliably close the output file descriptor and never write directly to the binary that is currently running.
Checkpoints to prevent recurrence:
- Drop direct
cp -fwrites in deploy scripts - stage under a temp name, then swap withmv/install. Many deploy tools (replacing ago buildoutput, etc.) already work this way - Stop, update, restart running services by default - if you need zero-downtime updates, use the rename approach, or consider systemd
ExecReload/ socket activation - Close the output fd before running a fresh build - chaining
make && ./outfails withETXTBSYif the build left a write handle open. Close it explicitly or split it into separate steps - Treat shared library updates the same way - never overwrite an in-use
.so; replace it with a rename. Package managers (apt/dnf) do this internally, so avoid manualcpover a.so
On some network filesystems such as NFS, ETXTBSY behavior is not always consistent for a binary running on a different host. When updating an executable on shared storage, the safe path is to stop the process on each host before swapping it in.