siyuan-note/siyuan · error

resource escapes the data directory

Error message

resource escapes the data directory: %s

What it means

The asset relink scan verifies that every resolved asset path (after symlink evaluation) still lives inside the workspace data directory. If the resolved real path is not a subpath of DataDir, or symlink evaluation fails, the resource is rejected so relinking never touches or rewrites references to files outside the workspace. This is a safety gate protecting the workspace boundary.

Solutions

  1. Move or copy the asset into the workspace data directory (e.g. data/assets/) so its real path is a subpath of DataDir
  2. Remove symlinks pointing outside the workspace and replace them with real files inside data/
  3. Fix the reference in the document so it uses a valid workspace-relative asset path
  4. Check DataDir resolution (e.g. wrong workspace opened) so legitimate assets are not falsely seen as escaping

Example fix

// before
relinkOld := "/mnt/shared/photos/logo.png" // outside data dir
// after
oldAbs := filepath.Join(util.DataDir, "assets", "logo.png")
err := filelock.Copy("/mnt/shared/photos/logo.png", oldAbs) // bring asset inside data/
relinkOld := "assets/logo.png"
Defensive patterns

Strategy: validation

Validate before calling

func isInsideDataDir(p string) bool {
	abs, err := filepath.Abs(p)
	if err != nil { return false }
	real, err := filepath.EvalSymlinks(abs)
	if err != nil { return false }
	root, err := filepath.EvalSymlinks(util.DataDir)
	return err == nil && gulu.File.IsSubPath(root, real)
}

Type guard

if !isInsideDataDir(oldPath) { return errors.New("path must resolve inside the workspace data directory") }

Prevention

When it happens

Trigger: Calling RelinkAssetWithContext/RelinkAssets or the scan functions (via resolveRelinkAsset, scanViews, scanAnnotation) with an oldPath whose absolute location is a symlink pointing outside data/, an escape like '../../outside.png', or a path whose EvalSymlinks fails.

Common situations: Assets symlinked from a shared folder outside the workspace; manually moved or hand-crafted .sy documents referencing paths with parent-directory escapes; cloud-sync setups where assets are junctions/symlinks to another volume; note-taking content pasted with absolute file paths later interpreted as assets.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/56763395daadc206. Report an issue: GitHub.

Appendix: source

Thrown at kernel/model/asset_relink.go:489

	for offset := 0; offset < 1000; offset++ {
		dir := filepath.Join(util.HistoryDir, time.Now().Add(time.Duration(offset)*time.Second).Format("2006-01-02-150405")+"-"+HistoryOpReplace)
		if err := os.Mkdir(dir, 0755); err == nil {
			return dir, nil
		} else if !os.IsExist(err) {
			return "", err
		}
	}
	return "", errors.New("cannot allocate a replacement history directory")
}

func validateRelinkStoragePath(abs string) error {
	real, err := filepath.EvalSymlinks(abs)
	if err != nil {
		return err
	}
	dataRoot, err := filepath.EvalSymlinks(util.DataDir)
	if err != nil || !gulu.File.IsSubPath(dataRoot, real) {
		return fmt.Errorf("resource escapes the data directory: %s", abs)
	}
	rel, err := filepath.Rel(dataRoot, real)
	if err != nil {
		return err
	}
	first, _, _ := strings.Cut(filepath.ToSlash(rel), "/")
	if ast.IsNodeIDPattern(first) && IsEncryptedBox(first) {
		return errors.New("encrypted resources are not supported")
	}
	return nil
}

View on GitHub (pinned to 9f775e8a12)