siyuan-note/siyuan · critical · errMasterPasswordMigrationPending

master password migration is pending: Master password change

Error message

master password migration is pending: Master password change partially failed. Please restart SiYuan to complete recovery. Detail: %s: rebuild encrypted conf from migration entry failed: %s

What it means

Returned by ChangeMasterPassword Phase 3 (crypto.go:1786-1802) when both the notebook conf.json and the per-notebook backup are unavailable, so the code rebuilds BoxCrypt directly from the migration manifest entry (the authoritative source of the new key material) - and that rebuild's box.SaveConf still fails. The migration manifest is preserved; restart completes recovery.

Source

Thrown at kernel/model/crypto.go:1800

				if saveErr := box.SaveConf(boxConf); saveErr != nil {
					return fmt.Errorf("%w: %s", errMasterPasswordMigrationPending,
						fmt.Sprintf(Conf.Language(320), entry.BoxID+": rebuild encrypted conf from backup failed: "+saveErr.Error()))
				}
			} else {
				// conf 与 backup 均不可用:manifest 是该 box 加密密钥的权威来源,直接从 entry 重建 BoxCrypt,
				// 避免改密因瞬时 conf 损坏而中断(详见 recoverMasterPasswordMigration 中的对称处理)。
				logging.LogWarnf("rebuild encrypted box [%s] from migration entry (conf and backup both unavailable)", entry.BoxID)
				boxConf = box.GetConf()
				boxConf.Encrypted = true
				boxConf.BoxCrypt = &conf.BoxEncryption{
					WrappedDEK: entry.NewWrappedDEK,
					WrapNonce:  entry.NewWrapNonce,
					Spec:       entry.NewSpec,
					Metadata:   entry.Metadata,
					CreatedAt:  time.Now().UnixMilli(),
				}
				if saveErr := box.SaveConf(boxConf); saveErr != nil {
					return fmt.Errorf("%w: %s", errMasterPasswordMigrationPending,
						fmt.Sprintf(Conf.Language(320), entry.BoxID+": rebuild encrypted conf from migration entry failed: "+saveErr.Error()))
				}
			}
		}
		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()))
		}
	}

View on GitHub (pinned to afa823b6b4)

Solutions

  1. Repair write access to <data>/<boxID>/.siyuan/ (space, permissions, locks) for the box named in the detail
  2. Restart SiYuan so migration recovery replays the manifest and retries the rebuild, then log in with the new password
  3. After recovery, verify the affected notebook unlocks and re-exports/backups its key material (a fresh crypt backup is written on next successful unlock)
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) {
    logMigrationDetail(err) // names the box whose conf+backup rebuild failed
    instructUserRestart()   // recovery replays the manifest entry for that box
    return
}

Prevention

When it happens

Trigger: A notebook whose conf.json is corrupt/unwritable and whose backup is missing/damaged, combined with a write failure (disk full, permissions, file lock) when saving the reconstructed conf.json during the password change.

Common situations: Notebooks already in a degraded state (deleted backup file plus damaged conf) hit during password rotation; failing disk; workspace permission changes made between sessions.

Related errors


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