@@ -168,7 +168,18 @@ def symlink(source, destination, force=False):
"""Create a symbolic link"""
try:
if force:
- remove(destination)
+ # Remove the exact destination path. Do not route this through
+ # remove(), which treats its argument as a glob pattern: a
+ # destination containing glob metacharacters (for example a
+ # '[' in the name) could fail to match, or match and delete
+ # unrelated files.
+ try:
+ os.unlink(destination)
+ except OSError as exc:
+ if exc.errno == errno.EISDIR:
+ shutil.rmtree(destination)
+ elif exc.errno != errno.ENOENT:
+ raise
os.symlink(source, destination)
except OSError as e:
if e.errno != errno.EEXIST or os.readlink(destination) != source:
symlink(source, destination, force=True) cleared an existing destination by calling remove(destination). remove() is a rm -rf helper that runs its argument through glob.glob() before unlinking, so it treats the destination as a glob pattern rather than a literal path. For ordinary names this is merely wasteful, but a destination that contains glob metacharacters is actively dangerous. A name with a '[' may fail to match itself and silently leave the old link in place, and a pattern that happens to match other entries could unlink files the caller never named. remove() even warns about exactly this in its own docstring. Remove the literal destination directly instead: unlink it, fall back to shutil.rmtree() when it turns out to be a directory (EISDIR), and treat a missing destination (ENOENT) as success. This mirrors what remove() does per matched name, minus the glob expansion. AI-Generated: codex/claude-opus 4.8 (xhigh) Signed-off-by: Trevor Woerner <twoerner@gmail.com> --- src/wic/oe/path.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-)