Skip to content

link2symlink: once a file name has used up its 999 intermediate suffixes, link() loses the file (reported as EPERM) #393

Description

@tn-py

Problem description

The first hard link link2symlink fakes for a file moves it to
<l2s dir>/.l2s.<name>NNNN.0002, NNNN being the first free four-digit suffix
for that name. The search stops at 999 (move_and_symlink_path()):

do {
	sprintf(new_intermediate, "%s%04d", intermediate, intermediate_suffix);
	intermediate_suffix++;
} while ((l2s_access(new_intermediate) != -1) && (intermediate_suffix < 1000));

When every suffix up to 0999 is taken, the loop ends on 0999, which is
in use, and the function carries on with it anyway:

  1. l2s_rename(original, ".l2s.<name>0999.0002") moves the tracee's file
    away, silently replacing that final file if it exists (it does whenever
    the file using 0999 currently has two links, e.g. while dpkg holds its
    backup).
  2. l2s_symlink(final, ".l2s.<name>0999") fails with EEXIST.
  3. The error is returned as-is. The l2s_* helpers return -1 with errno
    set, so the tracee gets -1, i.e. EPERM, instead of the real error.
  4. Nothing moves the file back, so the file is gone from where the
    tracee left it.

The same thing happens before the limit, too:

  • A dangling intermediate counts as free, because access() follows it.
    Creating the symlink over it then fails the same way and the file is lost.
  • A final file left without its intermediate counts as free and is
    silently replaced by the rename.

proot-distro pins PROOT_L2S_DIR to a single <rootfs>/.l2s, so every file
with the same name anywhere in the rootfs shares those 999 suffixes. dpkg
hard-links each file it replaces as a backup, so a Python-heavy system runs
out of __init__.py suffixes quickly (google-cloud-cli alone ships over
4,000). From then on every dpkg upgrade that replaces an __init__.py fails,
and each failed run deletes one file:

Unpacking google-cloud-cli (584.0.0-0) over (582.0.0-0) ...
dpkg: error processing archive .../google-cloud-cli_584.0.0-0_arm64.deb (--unpack):
 unable to make backup link of './usr/lib/google-cloud-sdk/lib/googlecloudsdk/command_lib/compute/network_policies/__init__.py' before installing new version: Operation not permitted

That system had exactly 999 .l2s.__init__.py0001...0999 entries in its
/.l2s. Five retries left five __init__.py files missing, one per run.

Steps to reproduce

From a plain Termux shell (no distro needed):

D=$TMPDIR/l2s-demo
rm -rf $D && mkdir -p $D/l2s $D/w
seq -f "$D/l2s/.l2s.__init__.py%04g" 1 999 | xargs touch   # use up the suffixes
echo data > $D/w/__init__.py
PROOT_L2S_DIR=$D/l2s proot -l ln $D/w/__init__.py $D/w/backup
cat $D/w/__init__.py
ls -A $D/l2s | tail -n 2

Result:

ln: failed to access '.../w/__init__.py': No such file or directory
cat: .../w/__init__.py: No such file or directory
.l2s.__init__.py0999
.l2s.__init__.py0999.0002        <- the file's content ended up here, unreferenced

Without the pre-filled directory, a single dangling intermediate is enough:

D=$TMPDIR/l2s-demo2
rm -rf $D && mkdir -p $D && cd $D
echo data > original
ln -s "$D/.l2s.original0001.0002" .l2s.original0001    # dangling intermediate
proot -l ln original link
cat original          # No such file or directory

Expected behavior

link() either succeeds or fails with a meaningful errno (EMLINK when no
suffix is left), and in both cases the original file stays where it was and
no other file's content is replaced.

Additional information

  • proot 5.1.107-71 (Termux package). Reproduced on current master (7266fb3)
    built from source; the code in question is unchanged there.
  • proot-distro 5.0.2, Debian 13 (trixie) aarch64 rootfs
  • Samsung SM-N975U, Android 10, unrooted
Packages CPU architecture: arm64
termux-tools version: 1.45.0
Android version: 10
Device manufacturer: samsung
Device model: SM-N975U
Supported ABIs: arm64-v8a,armeabi-v7a,armeabi

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions