siyuan-note/siyuan · critical

master password migration is pending

Error message

master password migration is pending: %v

What it means

Thrown by the KEK verification path (crypto.go ~1240-1287) the first time the user successfully verifies the NEW master password after ChangeMasterPassword crashed midway and left a migration manifest. Before ending the migration it must re-verify every notebook against the new KEK and write the authenticated global notebook-crypto backup (saveNotebookCryptoBackup); if that backup write fails, errMasterPasswordMigrationPending is returned wrapped with the underlying I/O error (%v). The migration manifest is kept on purpose, so recovery is re-attempted on the next start.

Solutions

  1. Fix the underlying write problem (free disk space, clear file locks/AV exclusions, restore write permission to the workspace), then restart SiYuan and re-enter the new master password - recovery re-runs automatically
  2. Check the kernel log for the wrapped %v detail to identify which file/I/O operation failed
  3. If the backup file itself is corrupted, restore the workspace (conf + notebook crypt backups) from an external snapshot and retry verification
  4. Report to the SiYuan repo with logs if verification keeps failing with the same detail after the disk issue is resolved
Defensive patterns

Strategy: type-guard

Type guard

// in-package (kernel/model):
func isMigrationPendingErr(err error) bool {
    return errors.Is(err, errMasterPasswordMigrationPending)
}

// outside the package (sentinel is unexported):
func isMigrationPendingErr(err error) bool {
    return err != nil && strings.HasPrefix(err.Error(), "master password migration is pending")
}

Try / catch

if err := verifyMasterPassword(pw); isMigrationPendingErr(err) {
    // transient completion failure: surface restart instruction, do NOT fall back to old password flows
    showUser("Password verified but recovery could not finish: " + err.Error() + " - free disk space and restart SiYuan, then re-enter the new password.")
    return
}

Prevention

When it happens

Trigger: ChangeMasterPassword was interrupted between Phase 2 (verifier switch) and Phase 4 (manifest removal); on restart the user submits the new master password; verification succeeds but saveNotebookCryptoBackup fails because the workspace/config directory is not writable (disk full, permission denied, file locked by antivirus, read-only media).

Common situations: Disk exhaustion during a password change; Windows AV or sync clients (OneDrive/Dropbox) locking conf/backup files; workspace on a network share that dropped mid-write; user force-quit SiYuan during the change and the disk condition persists on restart.

Related errors


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

Appendix: source

Thrown at kernel/model/crypto.go:1283

			Conf.m.Unlock()
			Conf.Save()
			logging.LogInfof("repaired notebook crypto configuration from authenticated backup")
		} else if !backupAuthenticated {
			// 同步备份可能属于另一轮完整改密;只要本地配置仍与全部笔记本一致,就继续使用本地配置,
			// 不覆盖候选备份,等待其余 WrappedDEK 同步完成后由新密码采用。
			logging.LogWarnf("notebook crypto backup differs from usable local configuration; keeping both candidates")
		}
	}

	if migrationPending {
		// 崩溃恢复后的首次新密码验证:确认所有笔记本都已切换到新 KEK,再生成带认证的全局备份并结束迁移。
		if !verifyKEKAgainstExistingBoxes(kek) {
			zeroAndClear(kek)
			return nil, errMasterPasswordMigrationPending
		}
		if err = saveNotebookCryptoBackup(kek); err != nil {
			zeroAndClear(kek)
			return nil, fmt.Errorf("%w: %v", errMasterPasswordMigrationPending, err)
		}
		removeMasterPasswordMigration()
	}
	return kek, nil
}

// decryptBoxCrypt 用 KEK 解密 box 的 WrappedDEK。优先使用 GetBoxEncryption 的结果(conf → backup fallback),
// 若解密失败则尝试 backup 中不同的 WrappedDEK。
// 返回解密后的 DEK 和实际使用的 BoxCrypt(可能来自 backup)。
// 若 backup 被使用会自动修复 conf.json 和刷新 backup。
func decryptBoxCrypt(boxID string, kek []byte) (dek []byte, boxCrypt *conf.BoxEncryption, err error) {
	boxCrypt, err = GetBoxEncryption(boxID)
	if err != nil || boxCrypt == nil || len(boxCrypt.WrappedDEK) == 0 {
		return nil, nil, fmt.Errorf("no encrypted key material for box [%s]", boxID)
	}

	dek, err = decryptWrappedDEK(boxID, boxCrypt, kek)
	if err == nil {

View on GitHub (pinned to afa823b6b4)