siyuan-note/siyuan · critical · errMasterPasswordMigrationPending

master password migration is pending: Master password…

Error message

master password migration is pending: Master password change partially failed. Please restart SiYuan to complete recovery. Detail: save notebook crypto backup failed: %s

What it means

Returned by ChangeMasterPassword Phase 4 (crypto.go:1819-1823) when the final authenticated global notebook-crypto backup (saveNotebookCryptoBackup) fails after all per-notebook confs and backups were already migrated to the new KEK. Because the manifest is only removed after this write succeeds, the change is deliberately left 'pending'; restarting and verifying the new password re-attempts exactly this step.

Solutions

  1. Fix the write condition on the global backup path (space, permissions, locks)
  2. Restart SiYuan and submit the new master password - verification detects the pending manifest, re-checks the boxes and retries saveNotebookCryptoBackup
  3. Only after recovery completes (no more 'migration is pending' on verification) treat the password change as done
Defensive patterns

Strategy: type-guard

Type guard

func isMigrationPendingErr(err error) bool {
    return errors.Is(err, errMasterPasswordMigrationPending) // in-package; outside use string prefix check
}

Try / catch

if err := model.ChangeMasterPassword(oldPw, newPw); isMigrationPendingErr(err) {
    // notebooks migrated but global backup failed: manifest retained on purpose,
    // restart + verify with the new password runs exactly this last step again
    instructUserRestart()
    return
}

Prevention

When it happens

Trigger: Global backup file write failure at the very end of the change: disk full, permission denied, or a lock on the global notebook crypto backup path - after all notebook-level writes already succeeded.

Common situations: Disk hitting its limit at the last write of a long migration; AV quarantining/locking the global backup file; workspace directory permission changed mid-operation.

Related errors


AI-assisted analysis of siyuan-note/siyuan@afa823b6b4 (2026-08-18). Data as JSON: /api/errors/4051cec5b5c596e6. Report an issue: GitHub.

Appendix: source

Thrown at kernel/model/crypto.go:1821

			}
		}
		boxConf.BoxCrypt.WrappedDEK = entry.NewWrappedDEK
		boxConf.BoxCrypt.Spec = entry.NewSpec
		boxConf.BoxCrypt.WrapNonce = entry.NewWrapNonce
		boxConf.BoxCrypt.Metadata = append([]byte(nil), entry.Metadata...)
		if err = box.SaveConf(boxConf); err != nil {
			return fmt.Errorf("%w: %s", errMasterPasswordMigrationPending,
				fmt.Sprintf(Conf.Language(320), entry.BoxID+": save conf failed: "+err.Error()))
		}
		if err = writeNotebookCryptBackup(entry.BoxID, boxConf.BoxCrypt); err != nil {
			return fmt.Errorf("%w: %s", errMasterPasswordMigrationPending,
				fmt.Sprintf(Conf.Language(320), entry.BoxID+": update notebook crypt backup failed: "+err.Error()))
		}
	}

	// Phase 4: 先持久化全局备份,再清除 manifest,确保崩溃后可恢复
	if err = saveNotebookCryptoBackup(newKEK); err != nil {
		return fmt.Errorf("%w: %s", errMasterPasswordMigrationPending,
			fmt.Sprintf(Conf.Language(320), "save notebook crypto backup failed: "+err.Error()))
	}
	removeMasterPasswordMigration()
	IncSync()
	return nil
}

// IsEncryptedBox 判断给定 boxID 是否为加密笔记本。
// 配置缺失或损坏时依次检查运行时身份、独立备份和密文标识,任何检查错误都按加密笔记本处理。
func IsEncryptedBox(boxID string) bool {
	if !ast.IsNodeIDPattern(boxID) {
		return false
	}
	if isRuntimeEncryptedBox(boxID) {
		return true
	}
	if isRuntimeNormalBox(boxID) {
		return false

View on GitHub (pinned to afa823b6b4)