MemPalace/mempalace · warning · ValueError

refusing to export: {label} is a symbolic link ({path!r}). R

Error message

refusing to export: {label} is a symbolic link ({path!r}). Remove the symlink or choose a different output path.

What it means

Raised by _reject_symlink before any export write when the target path itself is a symbolic link. This is defense-in-depth against symlink attacks: a pre-placed symlink at the export destination would redirect writes to wherever it points (system directories, other users' files). Exporter refuses and tells the user to remove the link or pick another path; the same caution mirrors the miner's input-side checks.

Source

Thrown at mempalace/exporter.py:38

from .palace import get_collection


def _safe_path_component(name: str) -> str:
    """Sanitize a string for use as a directory/file name component."""
    name = re.sub(r'[/\\:*?"<>|]', "_", name)
    name = name.strip(". ")
    return name or "unknown"


def _reject_symlink(path: str, label: str) -> None:
    """Refuse to write into a path that is itself a symlink.

    Defense-in-depth: a pre-placed symlink at the export target would
    redirect writes to wherever it points (e.g., system directories).
    Mirrors the miner's input-side caution.
    """
    if os.path.islink(path):
        raise ValueError(
            f"refusing to export: {label} is a symbolic link ({path!r}). "
            f"Remove the symlink or choose a different output path."
        )


def _safe_open_for_write(path: str, mode: str, encoding: str = "utf-8"):
    """Open a file for writing, refusing to follow a symlink at the target path.

    On POSIX (O_NOFOLLOW available) the open itself fails with ELOOP if path is
    a symlink — closing the TOCTOU window between an islink check and the open.
    On platforms without O_NOFOLLOW (Windows), pre-checks ``os.path.islink``,
    which is narrower than no check at all.
    """
    o_nofollow = getattr(os, "O_NOFOLLOW", 0)
    if o_nofollow:
        flags = os.O_WRONLY | os.O_CREAT | o_nofollow
        flags |= os.O_APPEND if "a" in mode else os.O_TRUNC
        try:

View on GitHub (pinned to 06cb6987f0)

Solutions

  1. Replace the symlink with a real directory: rm <link> && mkdir <dir> (or bind-mount if you need the target location)
  2. Or export to a fresh plain directory and copy/sync afterward: export to ./export && rsync -a ./export/ ~/Dropbox/export/
  3. Check for symlinks first: find <output_dir> -type l
  4. If you need files to appear at the linked location, symlink in the other direction (link name -> real export dir) after export completes

Example fix

# before
ln -s ~/Dropbox/mempalace-export ~/export-target
export_palace(palace_path, output_dir=os.path.expanduser('~/export-target'))
# after
rm ~/export-target
mkdir -p ~/export-target
export_palace(palace_path, output_dir=os.path.expanduser('~/export-target'))
rsync -a ~/export-target/ ~/Dropbox/mempalace-export/
Defensive patterns

Strategy: validation

Validate before calling

import os

def export_path_is_safe(output_dir: str) -> bool:
    p = os.path.expanduser(output_dir)
    return not os.path.islink(p) and all(not os.path.islink(os.path.join(r, d))
                                         for r, ds, fs in os.walk(p) for d in ds) if os.path.exists(p) else not os.path.islink(p)

Try / catch

try:
    export_palace(palace_path, output_dir=out)
except ValueError as e:
    if "refusing to export" in str(e):
        os.unlink(out); os.mkdir(out)  # after investigating where the link pointed
        export_palace(palace_path, output_dir=out)

Prevention

When it happens

Trigger: Calling export_palace(palace_path, output_dir) (or a per-wing/room export path) where output_dir or one of the wing/room file paths is a symlink — e.g. ~/mempalace-export symlinked to a Dropbox/www directory, or a leftover link created by a previous tool.

Common situations: Users symlinking the export dir into a synced folder (Dropbox/iCloud) or a web root; shared machines where an attacker pre-plants links; dotfiles managers that manage directories as symlinks; a previous export run left a symlink behind.

Related errors


AI-assisted analysis of MemPalace/mempalace@06cb6987f0 (2026-08-15). Data as JSON: /api/errors/379581d7842bcc83. Report an issue: GitHub.