Fixing "Text file busy"

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> or fuser <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 with systemctl rather 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 cp to overwrite and use mv (rename) or install to 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 with O_TRUNC and rewrites it in place -> ETXTBSY because it is running
  • mv new /usr/local/bin/myapp - renames a new inode to myapp and 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 -f writes in deploy scripts - stage under a temp name, then swap with mv / install. Many deploy tools (replacing a go build output, 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 && ./out fails with ETXTBSY if 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 manual cp over 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.

Summary / Keep reading