siyuan-note/siyuan · error

path is not a child of assets directory: %s

Error message

path is not a child of assets directory: %s

What it means

Thrown by ResolveDataAssetPath (kernel/model/assets.go:945) when the computed absolute path of an asset is not located beneath the assets root directory derived from the same input. It is a defense-in-depth invariant check that runs after the input has already been cleaned and confirmed to contain an `assets` path segment, so under normal operation it should never fire; when it does, it means the path and the detected assets root disagree (typically a malformed or adversarial path that survived earlier cleaning).

Source

Thrown at kernel/model/assets.go:945

			if !filelock.IsExist(boxConfPath) {
				err = fmt.Errorf("asset path does not belong to a notebook: %s", assetPath)
				return
			}
			if IsEncryptedBox(parts[0]) {
				err = fmt.Errorf("accessing assets in encrypted notebook [%s] is not supported", parts[0])
				return
			}
		}
	}
	if assetDirIndex < 0 {
		err = fmt.Errorf("path is not under an assets directory: %s", assetPath)
		return
	}

	assetRootParts := parts[:assetDirIndex+1]
	assetRoot := filepath.Join(util.DataDir, filepath.FromSlash(strings.Join(assetRootParts, "/")))
	if !gulu.File.IsSubPath(assetRoot, absPath) {
		err = fmt.Errorf("path is not a child of assets directory: %s", assetPath)
		return
	}

	resolvedRoot, evalErr := filepath.EvalSymlinks(assetRoot)
	if evalErr != nil {
		err = fmt.Errorf("resolve assets directory [%s] failed: %w", assetRoot, evalErr)
		return
	}
	if assetDirIndex > 0 {
		notebookRoot := filepath.Join(util.DataDir, parts[0])
		resolvedDataDir, dataEvalErr := filepath.EvalSymlinks(util.DataDir)
		resolvedNotebookRoot, notebookEvalErr := filepath.EvalSymlinks(notebookRoot)
		if dataEvalErr != nil || notebookEvalErr != nil ||
			!gulu.File.IsSubPath(resolvedDataDir, resolvedNotebookRoot) ||
			!gulu.File.IsSubPath(resolvedNotebookRoot, resolvedRoot) {
			err = fmt.Errorf("notebook asset path resolves outside notebook directory: %s", assetPath)
			return
		}

View on GitHub (pinned to 251596fc0d)

Solutions

  1. Inspect the exact assetPath string passed in (it is included via %s) and re-run it through filepath.Clean + filepath.Rel(util.DataDir, ...) manually to see why the assets-root membership check fails.
  2. Confirm util.DataDir is a real directory and not itself a broken/looping symlink; if it is, fix the workspace bootstrap so DataDir resolves to a stable physical directory.
  3. If you are a caller building the asset path dynamically, build it with filepath.Join on already-validated segments instead of string concatenation, and ensure it is relative to util.DataDir with an `assets/` prefix.
  4. Treat occurrences as a security signal: do not silence it, log the raw input and stack trace, and reject the request.
Defensive patterns

Strategy: validation

Validate before calling

// Reject input that cannot possibly be a data-relative asset path before calling ResolveDataAssetPath.
func validDataAssetPath(p string) error {
    if p == "" { return errors.New("asset path is required") }
    if filepath.IsAbs(p) { return errors.New("asset path must be relative") }
    cleaned := filepath.Clean(filepath.FromSlash(p))
    rel, err := filepath.Rel(util.DataDir, filepath.Join(util.DataDir, cleaned))
    if err != nil || rel == "." || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
        return errors.New("asset path escapes data directory")
    }
    return nil
}

Try / catch

relativePath, absPath, err := model.ResolveDataAssetPath(assetPath)
if err != nil {
    // log assetPath for audit; do not fall back to raw filesystem access
    logging.LogWarningf("resolve data asset failed: %s, err: %s", assetPath, err)
    return "", err
}

Prevention

When it happens

Trigger: Calling model.ResolveDataAssetPath (directly or via cli/cmd/asset.go, mcp/tools/asset.go, server/serve.go:919) with a path string that, after filepath.Clean + filepath.Rel against util.DataDir, contains an `assets` segment but whose resulting absPath is not a descendant of the reconstructed assets root. This is essentially unreachable through ordinary file paths because Clean+Rel already normalize traversal.

Common situations: Developers almost never hit this in production. If observed, it usually indicates either a programmatic caller constructing a non-standard asset path (e.g. embedding NUL bytes or OS-specific separators that confuse Split vs Join), a bug introduced when refactoring the path logic, or a filesystem where DataDir itself is a symlink that changes the Rel result unexpectedly.

Related errors


AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12). Data as JSON: /api/errors/22345e85a394a5fd. Report an issue: GitHub.