diff --git a/kadmin/init.c b/kadmin/init.c index b143d42c2..bcf61fefe 100644 --- a/kadmin/init.c +++ b/kadmin/init.c @@ -157,14 +157,11 @@ init(struct init_options *opt, int argc, char **argv) krb5_warn(context, ret, "hdb_open"); return 0; } - db->hdb_close(context, db); - ret = kadm5_log_init(kadm_handle); - if (ret) - krb5_err(context, 1, ret, "Failed iprop log initialization"); - ret = kadm5_log_nop(kadm_handle); + ret = kadm5_log_reinit(kadm_handle); if (ret) krb5_err(context, 1, ret, "Failed iprop log initialization"); kadm5_log_end(kadm_handle); + db->hdb_close(context, db); for(i = 0; i < argc; i++){ krb5_principal princ; const char *realm = argv[i]; diff --git a/kadmin/load.c b/kadmin/load.c index eb33be77a..d1d630b2e 100644 --- a/kadmin/load.c +++ b/kadmin/load.c @@ -377,10 +377,17 @@ doit(const char *filename, int mergep) krb5_warn(context, errno, "fopen(%s)", filename); return 1; } - ret = kadm5_log_truncate (kadm_handle); + /* + * We don't have a version number in the dump, so we don't know + * which iprop log entries to keep, if any. We throw the log away. + * + * We could merge the ipropd-master/slave dump/load here as an + * option, in which case we would first load the dump. + */ + ret = kadm5_log_reinit(kadm_handle); if (ret) { fclose (f); - krb5_warn(context, ret, "kadm5_log_truncate"); + krb5_warn(context, ret, "kadm5_log_reinit"); return 1; } @@ -535,6 +542,7 @@ doit(const char *filename, int mergep) break; } } + (void) kadm5_log_end(kadm_handle); db->hdb_close(context, db); fclose(f); return ret != 0; diff --git a/lib/hdb/common.c b/lib/hdb/common.c index fe0d4f909..150590c7b 100644 --- a/lib/hdb/common.c +++ b/lib/hdb/common.c @@ -321,6 +321,22 @@ _hdb_store(krb5_context context, HDB *db, unsigned flags, hdb_entry_ex *entry) if (code) return code; + if ((flags & HDB_F_PRECHECK) && (flags & HDB_F_REPLACE)) + return 0; + + if ((flags & HDB_F_PRECHECK)) { + code = hdb_principal2key(context, entry->entry.principal, &key); + if (code) + return code; + code = db->hdb__get(context, db, key, &value); + krb5_data_free(&key); + if (code == 0) + krb5_data_free(&value); + if (code == HDB_ERR_NOENTRY) + return 0; + return code ? code : HDB_ERR_EXISTS; + } + if(entry->entry.generation == NULL) { struct timeval t; entry->entry.generation = malloc(sizeof(*entry->entry.generation)); @@ -360,13 +376,32 @@ _hdb_store(krb5_context context, HDB *db, unsigned flags, hdb_entry_ex *entry) } krb5_error_code -_hdb_remove(krb5_context context, HDB *db, krb5_const_principal principal) +_hdb_remove(krb5_context context, HDB *db, + unsigned flags, krb5_const_principal principal) { - krb5_data key; + krb5_data key, value; int code; hdb_principal2key(context, principal, &key); + if ((flags & HDB_F_PRECHECK)) { + /* + * We don't check that we can delete the aliases because we + * assume that the DB is consistent. If we did check for alias + * consistency we'd also have to provide a way to fsck the DB, + * otherwise admins would have no way to recover -- papering + * over this here is less work, but we really ought to provide + * an HDB fsck. + */ + code = db->hdb__get(context, db, key, &value); + krb5_data_free(&key); + if (code == 0) { + krb5_data_free(&value); + return 0; + } + return code; + } + code = hdb_remove_aliases(context, db, &key); if (code) { krb5_data_free(&key); diff --git a/lib/hdb/db.c b/lib/hdb/db.c index 0b9e84337..904f41f97 100644 --- a/lib/hdb/db.c +++ b/lib/hdb/db.c @@ -211,6 +211,7 @@ DB__put(krb5_context context, HDB *db, int replace, k.size = key.length; v.data = value.data; v.size = value.length; + krb5_clear_error_message(context); code = (*d->put)(d, &k, &v, replace ? 0 : R_NOOVERWRITE); if(code < 0) { code = errno; @@ -219,9 +220,15 @@ DB__put(krb5_context context, HDB *db, int replace, return code; } if(code == 1) { - krb5_clear_error_message(context); return HDB_ERR_EXISTS; } + code = (*d->sync)(d, 0); + if (code == -1) { + code = errno; + krb5_set_error_message(context, code, "Database %s put sync error: %s", + db->hdb_name, strerror(code)); + return code; + } return 0; } @@ -233,15 +240,23 @@ DB__del(krb5_context context, HDB *db, krb5_data key) krb5_error_code code; k.data = key.data; k.size = key.length; + krb5_clear_error_message(context); code = (*d->del)(d, &k, 0); - if(code == 1) { + if (code == 1) + return HDB_ERR_NOENTRY; + if (code < 0) { code = errno; - krb5_set_error_message(context, code, "Database %s put error: %s", + krb5_set_error_message(context, code, "Database %s del error: %s", + db->hdb_name, strerror(code)); + return code; + } + code = (*d->sync)(d, 0); + if (code == -1) { + code = errno; + krb5_set_error_message(context, code, "Database %s del sync error: %s", db->hdb_name, strerror(code)); return code; } - if(code < 0) - return errno; return 0; } diff --git a/lib/hdb/db3.c b/lib/hdb/db3.c index f809febed..4b14e5a53 100644 --- a/lib/hdb/db3.c +++ b/lib/hdb/db3.c @@ -235,8 +235,46 @@ DB__put(krb5_context context, HDB *db, int replace, code = (*d->put)(d, NULL, &k, &v, replace ? 0 : DB_NOOVERWRITE); if(code == DB_KEYEXIST) return HDB_ERR_EXISTS; - if(code) - return errno; + if (code) { + /* + * Berkeley DB 3 and up have a terrible error reporting + * interface... + * + * DB->err() doesn't output a string. + * DB->set_errcall()'s callback function doesn't have a void * + * argument that can be used to place the error somewhere. + * + * The only thing we could do is fopen()/fdopen() a file, set it + * with DB->set_errfile(), then call DB->err(), then read the + * message from the file, unset it with DB->set_errfile(), close + * it and delete it. That's a lot of work... so we don't do it. + */ + if (code == EACCES || code == ENOSPC || code == EINVAL) { + krb5_set_error_message(context, code, + "Database %s put error: %s", + db->hdb_name, strerror(code)); + } else { + code = HDB_ERR_UK_SERROR; + krb5_set_error_message(context, code, + "Database %s put error: unknown (%d)", + db->hdb_name, code); + } + return code; + } + code = (*d->sync)(d, 0); + if (code) { + if (code == EACCES || code == ENOSPC || code == EINVAL) { + krb5_set_error_message(context, code, + "Database %s put sync error: %s", + db->hdb_name, strerror(code)); + } else { + code = HDB_ERR_UK_SERROR; + krb5_set_error_message(context, code, + "Database %s put sync error: unknown (%d)", + db->hdb_name, code); + } + return code; + } return 0; } @@ -253,8 +291,33 @@ DB__del(krb5_context context, HDB *db, krb5_data key) code = (*d->del)(d, NULL, &k, 0); if(code == DB_NOTFOUND) return HDB_ERR_NOENTRY; - if(code) + if (code) { + if (code == EACCES || code == ENOSPC || code == EINVAL) { + krb5_set_error_message(context, code, + "Database %s del error: %s", + db->hdb_name, strerror(code)); + } else { + code = HDB_ERR_UK_SERROR; + krb5_set_error_message(context, code, + "Database %s del error: unknown (%d)", + db->hdb_name, code); + } return code; + } + code = (*d->sync)(d, 0); + if (code) { + if (code == EACCES || code == ENOSPC || code == EINVAL) { + krb5_set_error_message(context, code, + "Database %s del sync error: %s", + db->hdb_name, strerror(code)); + } else { + code = HDB_ERR_UK_SERROR; + krb5_set_error_message(context, code, + "Database %s del sync error: unknown (%d)", + db->hdb_name, code); + } + return code; + } return 0; } diff --git a/lib/hdb/hdb-ldap.c b/lib/hdb/hdb-ldap.c index 995131b5f..9b92905be 100644 --- a/lib/hdb/hdb-ldap.c +++ b/lib/hdb/hdb-ldap.c @@ -1735,6 +1735,9 @@ LDAP_store(krb5_context context, HDB * db, unsigned flags, LDAPMessage *msg = NULL, *e = NULL; char *dn = NULL, *name = NULL; + if ((flags & HDB_F_PRECHECK)) + return 0; /* we can't guarantee whether we'll be able to perform it */ + ret = LDAP_principal2message(context, db, entry->entry.principal, &msg); if (ret == 0) e = ldap_first_entry(HDB2LDAP(db), msg); @@ -1806,13 +1809,17 @@ LDAP_store(krb5_context context, HDB * db, unsigned flags, } static krb5_error_code -LDAP_remove(krb5_context context, HDB *db, krb5_const_principal principal) +LDAP_remove(krb5_context context, HDB *db, + unsigned flags, krb5_const_principal principal) { krb5_error_code ret; LDAPMessage *msg, *e; char *dn = NULL; int rc, limit = LDAP_NO_LIMIT; + if ((flags & HDB_F_PRECHECK)) + return 0; /* we can't guarantee whether we'll be able to perform it */ + ret = LDAP_principal2message(context, db, principal, &msg); if (ret) goto out; diff --git a/lib/hdb/hdb-mitdb.c b/lib/hdb/hdb-mitdb.c index 4dfb769bd..f3a20d9ef 100644 --- a/lib/hdb/hdb-mitdb.c +++ b/lib/hdb/hdb-mitdb.c @@ -950,8 +950,24 @@ mdb_store(krb5_context context, HDB *db, unsigned flags, hdb_entry_ex *entry) krb5_data line = { 0, 0 }; krb5_data kdb_ent = { 0, 0 }; krb5_data key = { 0, 0 }; + krb5_data value = { 0, 0 }; ssize_t sz; + if ((flags & HDB_F_PRECHECK) && (flags & HDB_F_REPLACE)) + return 0; + + if ((flags & HDB_F_PRECHECK)) { + ret = mdb_principal2key(context, entry->entry.principal, &key); + if (ret) return ret; + code = db->hdb__get(context, db, key, &value); + krb5_data_free(&key); + if (code == 0) + krb5_data_free(&value); + if (code == HDB_ERR_NOENTRY) + return 0; + return code ? code : HDB_ERR_EXISTS; + } + sp = krb5_storage_emem(); if (!sp) return ENOMEM; ret = _hdb_set_master_key_usage(context, db, 0); /* MIT KDB uses KU 0 */ @@ -989,11 +1005,22 @@ out: } static krb5_error_code -mdb_remove(krb5_context context, HDB *db, krb5_const_principal principal) +mdb_remove(krb5_context context, HDB *db, + unsigned flags, krb5_const_principal principal) { krb5_error_code code; krb5_data key; + if ((flags & HDB_F_PRECHECK)) { + code = db->hdb__get(context, db, key, &value); + krb5_data_free(&key); + if (code == 0) { + krb5_data_free(&value); + return 0; + } + return code; + } + mdb_principal2key(context, principal, &key); code = db->hdb__del(context, db, key); krb5_data_free(&key); diff --git a/lib/hdb/hdb-sqlite.c b/lib/hdb/hdb-sqlite.c index 9bb885b87..ff387c3f7 100644 --- a/lib/hdb/hdb-sqlite.c +++ b/lib/hdb/hdb-sqlite.c @@ -672,24 +672,24 @@ hdb_sqlite_store(krb5_context context, HDB *db, unsigned flags, goto rollback; } - ret = 0; - commit: - krb5_data_free(&value); - sqlite3_clear_bindings(get_ids); sqlite3_reset(get_ids); + if ((flags & HDB_F_PRECHECK)) { + (void) hdb_sqlite_exec_stmt(context, hsdb, "ROLLBACK", ret); + return 0; + } + ret = hdb_sqlite_exec_stmt(context, hsdb, "COMMIT", EINVAL); if(ret != SQLITE_OK) krb5_warnx(context, "hdb-sqlite: COMMIT problem: %d: %s", ret, sqlite3_errmsg(hsdb->db)); - return ret; + return ret == SQLITE_OK ? 0 : HDB_ERR_UK_SERROR; rollback: - krb5_warnx(context, "hdb-sqlite: store rollback problem: %d: %s", ret, sqlite3_errmsg(hsdb->db)); @@ -865,27 +865,63 @@ hdb_sqlite_rename(krb5_context context, HDB *db, const char *new_name) */ static krb5_error_code hdb_sqlite_remove(krb5_context context, HDB *db, - krb5_const_principal principal) + unsigned flags, krb5_const_principal principal) { krb5_error_code ret; hdb_sqlite_db *hsdb = (hdb_sqlite_db*)(db->hdb_db); + sqlite3_stmt *get_ids = hsdb->get_ids; sqlite3_stmt *rm = hsdb->remove; bind_principal(context, principal, rm, 1); + ret = hdb_sqlite_exec_stmt(context, hsdb, + "BEGIN IMMEDIATE TRANSACTION", EINVAL); + if (ret != SQLITE_OK) { + (void) hdb_sqlite_exec_stmt(context, hsdb, "ROLLBACK", EINVAL); + ret = HDB_ERR_UK_SERROR; + krb5_set_error_message(context, ret, + "SQLite BEGIN TRANSACTION failed: %s", + sqlite3_errmsg(hsdb->db)); + return ret; + } + + if ((flags & HDB_F_PRECHECK)) { + ret = bind_principal(context, principal, get_ids, 1); + if (ret) + return ret; + + ret = hdb_sqlite_step(context, hsdb->db, get_ids); + sqlite3_clear_bindings(get_ids); + sqlite3_reset(get_ids); + if (ret == SQLITE_DONE) { + (void) hdb_sqlite_exec_stmt(context, hsdb, "ROLLBACK", EINVAL); + return HDB_ERR_NOENTRY; + } + } + ret = hdb_sqlite_step(context, hsdb->db, rm); + sqlite3_clear_bindings(rm); + sqlite3_reset(rm); if (ret != SQLITE_DONE) { - ret = EINVAL; + (void) hdb_sqlite_exec_stmt(context, hsdb, "ROLLBACK", ret); + ret = HDB_ERR_UK_SERROR; krb5_set_error_message(context, ret, "sqlite remove failed: %d", ret); - } else - ret = 0; + return ret; + } - sqlite3_clear_bindings(rm); - sqlite3_reset(rm); + if ((flags & HDB_F_PRECHECK)) { + (void) hdb_sqlite_exec_stmt(context, hsdb, "ROLLBACK", EINVAL); + return 0; + } - return ret; + ret = hdb_sqlite_exec_stmt(context, hsdb, "COMMIT", HDB_ERR_UK_SERROR); + if (ret != SQLITE_OK) + krb5_warnx(context, "hdb-sqlite: COMMIT problem: %ld: %s", + (long)HDB_ERR_UK_SERROR, sqlite3_errmsg(hsdb->db)); + + return 0; } /** diff --git a/lib/hdb/hdb.h b/lib/hdb/hdb.h index b5cf50a57..e67a4e062 100644 --- a/lib/hdb/hdb.h +++ b/lib/hdb/hdb.h @@ -65,6 +65,7 @@ enum hdb_lockop{ HDB_RLOCK, HDB_WLOCK }; #define HDB_F_ALL_KVNOS 2048 /* we want all the keys, live or not */ #define HDB_F_FOR_AS_REQ 4096 /* fetch is for a AS REQ */ #define HDB_F_FOR_TGS_REQ 8192 /* fetch is for a TGS REQ */ +#define HDB_F_PRECHECK 16384 /* check that the operation would succeed */ /* hdb_capability_flags */ #define HDB_CAP_F_HANDLE_ENTERPRISE_PRINCIPAL 1 @@ -154,7 +155,7 @@ typedef struct HDB { * Remove an entry from the database. */ krb5_error_code (*hdb_remove)(krb5_context, struct HDB*, - krb5_const_principal); + unsigned, krb5_const_principal); /** * As part of iteration, fetch one entry */ @@ -186,25 +187,33 @@ typedef struct HDB { /** * Get an hdb_entry from a classical DB backend * - * If the database is a classical DB (ie BDB, NDBM, GDBM, etc) - * backend, this function will take a principal key (krb5_data) - * and return all data related to principal in the return - * krb5_data. The returned encoded entry is of type hdb_entry or - * hdb_entry_alias. + * This function takes a principal key (krb5_data) and returns all + * data related to principal in the return krb5_data. The returned + * encoded entry is of type hdb_entry or hdb_entry_alias. */ krb5_error_code (*hdb__get)(krb5_context, struct HDB*, krb5_data, krb5_data*); /** * Store an hdb_entry from a classical DB backend * - * Same discussion as in @ref HDB::hdb__get + * This function takes a principal key (krb5_data) and encoded + * hdb_entry or hdb_entry_alias as the data to store. + * + * For a file-based DB, this must synchronize to disk when done. + * This is sub-optimal for kadm5_s_rename_principal(), and for + * kadm5_s_modify_principal() when using principal aliases; to + * improve this so that only one fsync() need be done + * per-transaction will require HDB API extensions. */ krb5_error_code (*hdb__put)(krb5_context, struct HDB*, int, krb5_data, krb5_data); /** * Delete and hdb_entry from a classical DB backend * - * Same discussion as in @ref HDB::hdb__get + * This function takes a principal key (krb5_data) naming the record + * to delete. + * + * Same discussion as in @ref HDB::hdb__put */ krb5_error_code (*hdb__del)(krb5_context, struct HDB*, krb5_data); /** @@ -263,7 +272,7 @@ typedef struct HDB { krb5_error_code (*hdb_check_s4u2self)(krb5_context, struct HDB *, hdb_entry_ex *, krb5_const_principal); }HDB; -#define HDB_INTERFACE_VERSION 8 +#define HDB_INTERFACE_VERSION 9 struct hdb_method { int version; diff --git a/lib/kadm5/admin.h b/lib/kadm5/admin.h index 0fe4e03b4..288616c8b 100644 --- a/lib/kadm5/admin.h +++ b/lib/kadm5/admin.h @@ -48,6 +48,10 @@ #define KADM5_STRUCT_VERSION 0 +/* For kadm5_log_get_version_fd() */ +#define LOG_VERSION_LAST -1 +#define LOG_VERSION_FIRST 1 + #include #define KRB5_KDB_DISALLOW_POSTDATED 0x00000001 diff --git a/lib/kadm5/chpass_s.c b/lib/kadm5/chpass_s.c index 1ee471473..4b873ed78 100644 --- a/lib/kadm5/chpass_s.c +++ b/lib/kadm5/chpass_s.c @@ -58,6 +58,10 @@ change(void *server_handle, return ret; } + ret = kadm5_log_init(context); + if (ret) + goto out; + ret = context->db->hdb_fetch_kvno(context->context, context->db, princ, HDB_F_DECRYPT|HDB_F_GET_ANY|HDB_F_ADMIN_DATA, 0, &ent); if(ret) @@ -138,21 +142,23 @@ change(void *server_handle, if (ret) goto out2; - ret = context->db->hdb_store(context->context, context->db, - HDB_F_REPLACE, &ent); - if (ret) - goto out2; - - kadm5_log_modify(context, &ent.entry, - KADM5_ATTRIBUTES | KADM5_PRINCIPAL | KADM5_MOD_NAME | - KADM5_MOD_TIME | KADM5_KEY_DATA | KADM5_KVNO | - KADM5_PW_EXPIRATION | KADM5_TL_DATA); + /* This logs the change for iprop and writes to the HDB */ + ret = kadm5_log_modify(context, &ent.entry, + KADM5_ATTRIBUTES | KADM5_PRINCIPAL | + KADM5_MOD_NAME | KADM5_MOD_TIME | + KADM5_KEY_DATA | KADM5_KVNO | + KADM5_PW_EXPIRATION | KADM5_TL_DATA); out2: hdb_free_entry(context->context, &ent); out: - if (!context->keep_open) - context->db->hdb_close(context->context, context->db); + (void) kadm5_log_end(context); + if (!context->keep_open) { + kadm5_ret_t ret2; + ret2 = context->db->hdb_close(context->context, context->db); + if (ret == 0 && ret2 != 0) + ret = ret2; + } return _kadm5_error_code(ret); } @@ -208,6 +214,11 @@ kadm5_s_chpass_principal_with_key(void *server_handle, if(ret) return ret; } + + ret = kadm5_log_init(context); + if (ret) + goto out; + ret = context->db->hdb_fetch_kvno(context->context, context->db, princ, 0, HDB_F_GET_ANY|HDB_F_ADMIN_DATA, &ent); if(ret == HDB_ERR_NOENTRY) @@ -243,22 +254,21 @@ kadm5_s_chpass_principal_with_key(void *server_handle, hdb_replace_extension(context->context, &ent.entry, &ext); } - - ret = context->db->hdb_store(context->context, context->db, - HDB_F_REPLACE, &ent); - if (ret) - goto out2; - - kadm5_log_modify (context, - &ent.entry, - KADM5_PRINCIPAL | KADM5_MOD_NAME | KADM5_MOD_TIME | - KADM5_KEY_DATA | KADM5_KVNO | KADM5_PW_EXPIRATION | - KADM5_TL_DATA); + /* This logs the change for iprop and writes to the HDB */ + ret = kadm5_log_modify(context, &ent.entry, + KADM5_PRINCIPAL | KADM5_MOD_NAME | + KADM5_MOD_TIME | KADM5_KEY_DATA | KADM5_KVNO | + KADM5_PW_EXPIRATION | KADM5_TL_DATA); out2: hdb_free_entry(context->context, &ent); out: - if (!context->keep_open) - context->db->hdb_close(context->context, context->db); + (void) kadm5_log_end(context); + if (!context->keep_open) { + kadm5_ret_t ret2; + ret2 = context->db->hdb_close(context->context, context->db); + if (ret == 0 && ret2 != 0) + ret = ret2; + } return _kadm5_error_code(ret); } diff --git a/lib/kadm5/create_s.c b/lib/kadm5/create_s.c index ae6da4aa3..1646b83eb 100644 --- a/lib/kadm5/create_s.c +++ b/lib/kadm5/create_s.c @@ -127,23 +127,38 @@ kadm5_s_create_principal_with_key(void *server_handle, if(ret) goto out; + if (!context->keep_open) { + ret = context->db->hdb_open(context->context, context->db, O_RDWR, 0); + if (ret) + goto out; + } + + ret = kadm5_log_init(context); + if (ret) + goto out; + + ret = context->db->hdb_fetch_kvno(context->context, context->db, + princ->principal, 0, 0, &ent); + if (ret == 0) { + ret = HDB_ERR_EXISTS; + goto out; + } + ret = hdb_seal_keys(context->context, context->db, &ent.entry); if (ret) goto out; - if (!context->keep_open) { - ret = context->db->hdb_open(context->context, context->db, O_RDWR, 0); - if(ret) - goto out; - } - ret = context->db->hdb_store(context->context, context->db, 0, &ent); - if (!context->keep_open) - context->db->hdb_close(context->context, context->db); - if (ret) - goto out; - kadm5_log_create (context, &ent.entry); + /* This logs the change for iprop and writes to the HDB */ + ret = kadm5_log_create(context, &ent.entry); out: + (void) kadm5_log_end(context); + if (!context->keep_open) { + kadm5_ret_t ret2; + ret2 = context->db->hdb_close(context->context, context->db); + if (ret == 0 && ret2 != 0) + ret = ret2; + } hdb_free_entry(context->context, &ent); return _kadm5_error_code(ret); } @@ -174,9 +189,26 @@ kadm5_s_create_principal(void *server_handle, | KADM5_AUX_ATTRIBUTES | KADM5_KEY_DATA | KADM5_POLICY_CLR | KADM5_LAST_SUCCESS | KADM5_LAST_FAILED | KADM5_FAIL_AUTH_COUNT); - if(ret) + if (ret) goto out; + if (!context->keep_open) { + ret = context->db->hdb_open(context->context, context->db, O_RDWR, 0); + if (ret) + goto out; + } + + ret = kadm5_log_init(context); + if (ret) + goto out; + + ret = context->db->hdb_fetch_kvno(context->context, context->db, + princ->principal, 0, 0, &ent); + if (ret == 0) { + ret = HDB_ERR_EXISTS; + goto out; + } + ent.entry.keys.len = 0; ent.entry.keys.val = NULL; @@ -188,20 +220,17 @@ kadm5_s_create_principal(void *server_handle, if (ret) goto out; - if (!context->keep_open) { - ret = context->db->hdb_open(context->context, context->db, O_RDWR, 0); - if(ret) - goto out; - } - ret = context->db->hdb_store(context->context, context->db, 0, &ent); - if (!context->keep_open) - context->db->hdb_close(context->context, context->db); - if (ret) - goto out; - - kadm5_log_create (context, &ent.entry); + /* This logs the change for iprop and writes to the HDB */ + ret = kadm5_log_create(context, &ent.entry); out: + (void) kadm5_log_end(context); + if (!context->keep_open) { + kadm5_ret_t ret2; + ret2 = context->db->hdb_close(context->context, context->db); + if (ret == 0 && ret2 != 0) + ret = ret2; + } hdb_free_entry(context->context, &ent); return _kadm5_error_code(ret); } diff --git a/lib/kadm5/delete_s.c b/lib/kadm5/delete_s.c index 20180d6dd..1edbea2e0 100644 --- a/lib/kadm5/delete_s.c +++ b/lib/kadm5/delete_s.c @@ -50,11 +50,16 @@ kadm5_s_delete_principal(void *server_handle, krb5_principal princ) return ret; } } + + ret = kadm5_log_init(context); + if (ret) + goto out; + ret = context->db->hdb_fetch_kvno(context->context, context->db, princ, HDB_F_DECRYPT|HDB_F_GET_ANY|HDB_F_ADMIN_DATA, 0, &ent); - if(ret == HDB_ERR_NOENTRY) + if (ret == HDB_ERR_NOENTRY) goto out; - if(ent.entry.flags.immutable) { + if (ent.entry.flags.immutable) { ret = KADM5_PROTECT_PRINCIPAL; goto out2; } @@ -63,16 +68,18 @@ kadm5_s_delete_principal(void *server_handle, krb5_principal princ) if (ret) goto out2; - ret = context->db->hdb_remove(context->context, context->db, princ); - if (ret) - goto out2; - - kadm5_log_delete (context, princ); + /* This logs the change for iprop and writes to the HDB */ + ret = kadm5_log_delete(context, princ); out2: hdb_free_entry(context->context, &ent); out: - if (!context->keep_open) - context->db->hdb_close(context->context, context->db); + (void) kadm5_log_end(context); + if (!context->keep_open) { + kadm5_ret_t ret2; + ret2 = context->db->hdb_close(context->context, context->db); + if (ret == 0 && ret2 != 0) + ret = ret2; + } return _kadm5_error_code(ret); } diff --git a/lib/kadm5/get_s.c b/lib/kadm5/get_s.c index a4e1d8259..3150e5bdb 100644 --- a/lib/kadm5/get_s.c +++ b/lib/kadm5/get_s.c @@ -125,17 +125,37 @@ kadm5_s_get_principal(void *server_handle, kadm5_server_context *context = server_handle; kadm5_ret_t ret; hdb_entry_ex ent; + int hdb_is_rw = 1; memset(&ent, 0, sizeof(ent)); if (!context->keep_open) { - ret = context->db->hdb_open(context->context, context->db, O_RDONLY, 0); + ret = context->db->hdb_open(context->context, context->db, O_RDWR, 0); + if (ret == EPERM || ret == EACCES) { + ret = context->db->hdb_open(context->context, context->db, O_RDONLY, 0); + hdb_is_rw = 0; + } if(ret) return ret; } + + /* + * Attempt to recover the log. This will generally fail on slaves, + * and we can't tell if we're on a slave here. + * + * Perhaps we could set a flag in the kadm5_server_context to + * indicate whether a read has been done without recovering the log, + * in which case we could fail any subsequent writes. + */ + if (hdb_is_rw) + (void) kadm5_log_init_nb(context); + ret = context->db->hdb_fetch_kvno(context->context, context->db, princ, HDB_F_DECRYPT|HDB_F_ALL_KVNOS| HDB_F_GET_ANY|HDB_F_ADMIN_DATA, 0, &ent); + if (hdb_is_rw) + kadm5_log_end(context); + if (!context->keep_open) context->db->hdb_close(context->context, context->db); if(ret) diff --git a/lib/kadm5/iprop-commands.in b/lib/kadm5/iprop-commands.in index 78d88c9e3..5b59e5a33 100644 --- a/lib/kadm5/iprop-commands.in +++ b/lib/kadm5/iprop-commands.in @@ -41,6 +41,18 @@ command = { help = "configuration file" argument = "file" } + option = { + long = "no-lock" + short = "n" + type = "flag" + help = "don't lock iprop log" + } + option = { + long = "reverse" + short = "R" + type = "flag" + help = "dump the log in reverse order" + } option = { long = "realm" short = "r" @@ -49,7 +61,7 @@ command = { } function = "iprop_dump" help = "Prints the iprop transaction log in text." - max_args = "0" + max_args = "1" } command = { name = "truncate" @@ -66,9 +78,30 @@ command = { type = "string" help = "realm" } + option = { + long = "keep-entries" + short = "K" + type = "integer" + help = "number of entries to keep" + default = "-1" + } + option = { + long = "max-bytes" + short = "B" + type = "integer" + help = "keep entries that fit in the given number of bytes" + default = "-1" + } + option = { + long = "reset" + short = "R" + type = "flag" + help = "reset the log to version 1; forces full propagation" + default = "0"; + } function = "iprop_truncate" - help = "Truncate the log, preserve the version number." - max_args = "0" + help = "Truncate the log, preserve the version number. Keeps 100 entries by default." + max_args = "1" } command = { name = "replay" @@ -101,7 +134,7 @@ command = { } function = "iprop_replay" help = "Replay the log on the database." - max_args = "0" + max_args = "1" } command = { name = "last-version" @@ -112,13 +145,37 @@ command = { help = "configuration file" argument = "file" } + option = { + long = "no-lock" + short = "n" + type = "flag" + help = "don't lock iprop log" + } + option = { + long = "realm" + short = "r" + type = "string" + help = "realm" + } + function = "last_version" + help = "Print the last version of the log-file." +} +command = { + name = "signal" + option = { + long = "config-file" + short = "c" + type = "string" + help = "configuration file" + argument = "file" + } option = { long = "realm" short = "r" type = "string" help = "realm" } - function = "last_version" + function = "signal_master" help = "Print the last version of the log-file." max_args = "0" } diff --git a/lib/kadm5/iprop-log.8 b/lib/kadm5/iprop-log.8 index 7f84b0909..47c6a5aff 100644 --- a/lib/kadm5/iprop-log.8 +++ b/lib/kadm5/iprop-log.8 @@ -38,7 +38,7 @@ .Os Heimdal .Sh NAME .Nm iprop-log -.Nd maintain the iprop log file +.Nd examine and maintain the iprop log file .Sh SYNOPSIS .Nm .Op Fl Fl version @@ -54,7 +54,20 @@ .Fl Fl realm= Ns Ar string .Xc .Oc +.Oo Fl K Ar integer \*(Ba Xo +.Fl Fl keep-entries= Ns Ar integer +.Xc +.Oc +.Oo Fl B Ar integer \*(Ba Xo +.Fl Fl max-bytes= Ns Ar integer +.Xc +.Oc +.Oo Fl R \*(Ba Xo +.Fl Fl reset +.Xc +.Oc .Op Fl h | Fl Fl help +.Op Ar log-file .Pp .Nm iprop-log dump .Oo Fl c Ar file \*(Ba Xo @@ -65,7 +78,16 @@ .Fl Fl realm= Ns Ar string .Xc .Oc +.Oo Fl n \*(Ba Xo +.Fl Fl no-lock +.Xc +.Oc +.Oo Fl R \*(Ba Xo +.Fl Fl reverse +.Xc +.Oc .Op Fl h | Fl Fl help +.Op Ar log-file .Pp .Nm iprop-log replay .Op Fl Fl start-version= Ns Ar version-number @@ -79,6 +101,35 @@ .Xc .Oc .Op Fl h | Fl Fl help +.Op Ar log-file +.Pp +.Nm iprop-log last-version +.Oo Fl c Ar file \*(Ba Xo +.Fl Fl config-file= Ns Ar file +.Xc +.Oc +.Oo Fl r Ar string \*(Ba Xo +.Fl Fl realm= Ns Ar string +.Xc +.Oc +.Oo Fl n \*(Ba Xo +.Fl Fl no-lock +.Xc +.Oc +.Op Fl h | Fl Fl help +.Op Ar log-files +.Pp +.Nm iprop-log signal +.Oo Fl c Ar file \*(Ba Xo +.Fl Fl config-file= Ns Ar file +.Xc +.Oc +.Oo Fl r Ar string \*(Ba Xo +.Fl Fl realm= Ns Ar string +.Xc +.Oc +.Op Fl h | Fl Fl help +.Pp .Sh DESCRIPTION Supported options: .Bl -tag -width Ds @@ -94,11 +145,42 @@ command can be one of the following: configuration file .It Fl r Ar string , Fl Fl realm= Ns Ar string realm +.It Xo +.Fl K Ar integer , +.Fl Fl keep-entries= Ns Ar integer +.Xc +.It Xo +.Fl B Ar integer , +.Fl Fl max-bytes= Ns Ar integer +.Xc +.It Xo +.Fl R , +.Fl Fl reset +.Xc .El .Pp -Truncates the log. Sets the new logs version number for the to the -last entry of the old log. If the log is truncted by emptying the -file, the log will start over at the first version (0). +If +.Fl Fl reset +is given, then the given, configured, or default log file will be +truncated and will start at version 1. This forces full propagations to +all slave KDCs. +.Pp +Otherwise the log will be truncated but some entries will be preserved, +as specified by the +.Fl Fl keep-entries +and/or +.Fl Fl max-bytes +options. The largest number of +.Fl Fl keep-entries +entries that are available and fit in the given +.Fl Fl max-bytes +option will be used. The +.Fl Fl keep-entries option defaults to 100, and the +.Fl Fl max-bytes +option defaults to the +.Ar log-max-size +parameter in the configuration. +.Pp .It dump .Bl -tag -width Ds .It Fl c Ar file , Fl Fl config-file= Ns Ar file @@ -108,9 +190,23 @@ configuration file .Fl Fl realm= Ns Ar string .Xc realm +.It Xo +.Fl n Ar string , +.Fl Fl no-lock +.Xc +.It Xo +.Fl R Ar string , +.Fl Fl reverse +.Xc .El .Pp -Print out all entries in the log to standard output. +Print out all entries in the given, configured, or default log file to +standard output. If the +.Fl n +option is used then don't lock the iprop log file. If the +.Fl R +option is used, then print the entries in reverse order +(this can be useful when the log is very large). .It replay .Bl -tag -width Ds .It Fl Fl start-version= Ns Ar version-number @@ -125,17 +221,34 @@ configuration file realm .El .Pp -Replay the changes from specified entries (or all if none is -specified) in the transaction log to the database. +Replay the changes from specified entries (or all if none is specified) +in the given, configured, or default transaction log file to the +database. .It last-version .Bl -tag -width Ds .It Fl c Ar file , Fl Fl config-file= Ns Ar file configuration file .It Fl r Ar string , Fl Fl realm= Ns Ar string realm +.It Xo +.Fl n Ar string , +.Fl Fl no-lock +.Xc +.El +.Pp +prints the version of the last record in each of the given log files, or +the configured, or the default log file if none is given. +.It signal +.Bl -tag -width Ds +.It Fl c Ar file , Fl Fl config-file= Ns Ar file +configuration file +.It Fl r Ar string , Fl Fl realm= Ns Ar string +realm .El .Pp -prints the version of the last log entry. +Signals the ipropd-master daemon to send updates to slaves. Normally +kadmin does this every time it writes to the database, so this should +rarely be needed. .El .Sh SEE ALSO .Xr iprop 8 diff --git a/lib/kadm5/iprop-log.c b/lib/kadm5/iprop-log.c index b7c8cc9ac..dbe35e45f 100644 --- a/lib/kadm5/iprop-log.c +++ b/lib/kadm5/iprop-log.c @@ -46,11 +46,11 @@ get_kadmin_context(const char *config_file, char *realm) kadm5_config_params conf; krb5_error_code ret; void *kadm_handle; + char *file = NULL; char **files; int aret; if (config_file == NULL) { - char *file; aret = asprintf(&file, "%s/kdc.conf", hdb_db_dir(context)); if (aret == -1 || file == NULL) errx(1, "out of memory"); @@ -58,6 +58,7 @@ get_kadmin_context(const char *config_file, char *realm) } ret = krb5_prepend_config_files_default(config_file, &files); + free(file); if (ret) krb5_err(context, 1, ret, "getting configuration files"); @@ -102,7 +103,7 @@ static const char *op_names[] = { "nop" }; -static void +static kadm5_ret_t print_entry(kadm5_server_context *server_context, uint32_t ver, time_t timestamp, @@ -112,29 +113,28 @@ print_entry(kadm5_server_context *server_context, void *ctx) { char t[256]; + const char *entry_kind = ctx; int32_t mask; + int32_t nop_time; + uint32_t nop_ver; hdb_entry ent; krb5_principal source; char *name1, *name2; krb5_data data; krb5_context scontext = server_context->context; + krb5_error_code ret; krb5_data_zero(&data); - off_t end = krb5_storage_seek(sp, 0, SEEK_CUR) + len; - - krb5_error_code ret; - strftime(t, sizeof(t), "%Y-%m-%d %H:%M:%S", localtime(×tamp)); if((int)op < (int)kadm_get || (int)op > (int)kadm_nop) { printf("unknown op: %d\n", op); - krb5_storage_seek(sp, end, SEEK_SET); - return; + return 0; } - printf ("%s: ver = %u, timestamp = %s, len = %u\n", - op_names[op], ver, t, len); + printf ("%s%s: ver = %u, timestamp = %s, len = %u\n", + entry_kind, op_names[op], ver, t, len); switch(op) { case kadm_delete: krb5_ret_principal(sp, &source); @@ -261,12 +261,29 @@ print_entry(kadm5_server_context *server_context, free_hdb_entry(&ent); break; case kadm_nop : + if (len == 16) { + uint64_t off; + krb5_ret_uint64(sp, &off); + printf("uberblock offset %llu ", (unsigned long long)off); + } else { + printf("nop"); + } + if (len == 16 || len == 8) { + krb5_ret_int32(sp, &nop_time); + krb5_ret_uint32(sp, &nop_ver); + + timestamp = nop_time; + strftime(t, sizeof(t), "%Y-%m-%d %H:%M:%S", localtime(×tamp)); + printf("timestamp %s version %u", t, nop_ver); + } + printf("\n"); break; default: abort(); } krb5_data_free(&data); - krb5_storage_seek(sp, end, SEEK_SET); + + return 0; } int @@ -274,21 +291,61 @@ iprop_dump(struct dump_options *opt, int argc, char **argv) { kadm5_server_context *server_context; krb5_error_code ret; + enum kadm_iter_opts iter_opts_1st = 0; + enum kadm_iter_opts iter_opts_2nd = 0; + char *desc_1st = ""; + char *desc_2nd = ""; server_context = get_kadmin_context(opt->config_file_string, opt->realm_string); - ret = kadm5_log_init (server_context); - if (ret) - krb5_err (context, 1, ret, "kadm5_log_init"); + if (argc > 0) { + free(server_context->log_context.log_file); + server_context->log_context.log_file = strdup(argv[0]); + if (server_context->log_context.log_file == NULL) + krb5_err(context, 1, errno, "strdup"); + } - ret = kadm5_log_foreach (server_context, print_entry, NULL); - if(ret) + if (opt->reverse_flag) { + iter_opts_1st = kadm_backward | kadm_unconfirmed; + iter_opts_2nd = kadm_backward | kadm_confirmed; + desc_1st = "unconfirmed "; + } else { + iter_opts_1st = kadm_forward | kadm_confirmed; + iter_opts_2nd = kadm_forward | kadm_unconfirmed; + desc_2nd = "unconfirmed"; + } + + if (opt->no_lock_flag) { + ret = kadm5_log_init_sharedlock(server_context, LOCK_NB); + if (ret == EAGAIN || ret == EWOULDBLOCK) { + warnx("Not locking the iprop log"); + ret = kadm5_log_init_nolock(server_context); + if (ret) + krb5_err(context, 1, ret, "kadm5_log_init_nolock"); + } + } else { + warnx("If this command appears to block, try the --no-lock option"); + ret = kadm5_log_init_sharedlock(server_context, 0); + if (ret) + krb5_err(context, 1, ret, "kadm5_log_init_sharedlock"); + } + + ret = kadm5_log_foreach(server_context, iter_opts_1st, + NULL, print_entry, desc_1st); + if (ret) + krb5_warn(context, ret, "kadm5_log_foreach"); + + ret = kadm5_log_foreach(server_context, iter_opts_2nd, + NULL, print_entry, desc_2nd); + if (ret) krb5_warn(context, ret, "kadm5_log_foreach"); ret = kadm5_log_end (server_context); if (ret) krb5_warn(context, ret, "kadm5_log_end"); + + kadm5_destroy(server_context); return 0; } @@ -301,10 +358,38 @@ iprop_truncate(struct truncate_options *opt, int argc, char **argv) server_context = get_kadmin_context(opt->config_file_string, opt->realm_string); - ret = kadm5_log_truncate (server_context); - if (ret) - krb5_err (context, 1, ret, "kadm5_log_truncate"); + if (argc > 0) { + free(server_context->log_context.log_file); + server_context->log_context.log_file = strdup(argv[0]); + if (server_context->log_context.log_file == NULL) + krb5_err(context, 1, errno, "strdup"); + } + if (opt->keep_entries_integer < 0 && + opt->max_bytes_integer < 0) { + opt->keep_entries_integer = 100; + opt->max_bytes_integer = 0; + } + if (opt->keep_entries_integer < 0) + opt->keep_entries_integer = 0; + if (opt->max_bytes_integer < 0) + opt->max_bytes_integer = 0; + + if (opt->reset_flag) { + ret = kadm5_log_reinit(server_context); + } else { + ret = kadm5_log_init(server_context); + if (ret) + krb5_err(context, 1, ret, "kadm5_log_init"); + ret = kadm5_log_truncate(server_context, opt->keep_entries_integer, + opt->max_bytes_integer); + } + if (ret) + krb5_err(context, 1, ret, "kadm5_log_truncate"); + + kadm5_log_signal_master(server_context); + + kadm5_destroy(server_context); return 0; } @@ -312,26 +397,72 @@ int last_version(struct last_version_options *opt, int argc, char **argv) { kadm5_server_context *server_context; + char *alt_argv[2]; krb5_error_code ret; uint32_t version; + size_t i; server_context = get_kadmin_context(opt->config_file_string, opt->realm_string); - ret = kadm5_log_init (server_context); - if (ret) - krb5_err (context, 1, ret, "kadm5_log_init"); + if (argc == 0) { + alt_argv[0] = strdup(server_context->log_context.log_file); + alt_argv[1] = NULL; + if (alt_argv[0] == NULL) + krb5_err(context, 1, errno, "strdup"); + argv = alt_argv; + argc = 1; + } - ret = kadm5_log_get_version (server_context, &version); - if (ret) - krb5_err (context, 1, ret, "kadm5_log_get_version"); + for (i = 0; i < argc; i++) { + free(server_context->log_context.log_file); + server_context->log_context.log_file = strdup(argv[i]); + if (server_context->log_context.log_file == NULL) + krb5_err(context, 1, errno, "strdup"); - ret = kadm5_log_end (server_context); - if (ret) - krb5_warn(context, ret, "kadm5_log_end"); + if (opt->no_lock_flag) { + ret = kadm5_log_init_sharedlock(server_context, LOCK_NB); + if (ret == EAGAIN || ret == EWOULDBLOCK) { + warnx("Not locking the iprop log"); + ret = kadm5_log_init_nolock(server_context); + if (ret) + krb5_err(context, 1, ret, "kadm5_log_init_nolock"); + } + } else { + if (argc == 0) + warnx("If this command appears to block, try the " + "--no-lock option"); + ret = kadm5_log_init_sharedlock(server_context, 0); + if (ret) + krb5_err(context, 1, ret, "kadm5_log_init_sharedlock"); + } - printf("version: %lu\n", (unsigned long)version); + ret = kadm5_log_get_version (server_context, &version); + if (ret) + krb5_err (context, 1, ret, "kadm5_log_get_version"); + ret = kadm5_log_end (server_context); + if (ret) + krb5_warn(context, ret, "kadm5_log_end"); + + printf("version: %lu\n", (unsigned long)version); + } + + kadm5_destroy(server_context); + return 0; +} + +int +signal_master(struct signal_options *opt, int argc, char **argv) +{ + kadm5_server_context *server_context; + + server_context = get_kadmin_context(opt->config_file_string, + opt->realm_string); + + kadm5_log_signal_master(server_context); + + kadm5_destroy(server_context); return 0; } @@ -342,7 +473,7 @@ last_version(struct last_version_options *opt, int argc, char **argv) int start_version = -1; int end_version = -1; -static void +static kadm5_ret_t apply_entry(kadm5_server_context *server_context, uint32_t ver, time_t timestamp, @@ -357,18 +488,18 @@ apply_entry(kadm5_server_context *server_context, if((opt->start_version_integer != -1 && ver < (uint32_t)opt->start_version_integer) || (opt->end_version_integer != -1 && ver > (uint32_t)opt->end_version_integer)) { /* XXX skip this entry */ - krb5_storage_seek(sp, len, SEEK_CUR); - return; + return 0; } printf ("ver %u... ", ver); fflush (stdout); - ret = kadm5_log_replay (server_context, - op, ver, len, sp); + ret = kadm5_log_replay(server_context, op, ver, len, sp); if (ret) krb5_warn (server_context->context, ret, "kadm5_log_replay"); printf ("done\n"); + + return 0; } int @@ -380,6 +511,13 @@ iprop_replay(struct replay_options *opt, int argc, char **argv) server_context = get_kadmin_context(opt->config_file_string, opt->realm_string); + if (argc > 0) { + free(server_context->log_context.log_file); + server_context->log_context.log_file = strdup(argv[0]); + if (server_context->log_context.log_file == NULL) + krb5_err(context, 1, errno, "strdup"); + } + ret = server_context->db->hdb_open(context, server_context->db, O_RDWR | O_CREAT, 0600); @@ -390,7 +528,9 @@ iprop_replay(struct replay_options *opt, int argc, char **argv) if (ret) krb5_err (context, 1, ret, "kadm5_log_init"); - ret = kadm5_log_foreach (server_context, apply_entry, opt); + ret = kadm5_log_foreach(server_context, + kadm_forward | kadm_confirmed | kadm_unconfirmed, + NULL, apply_entry, opt); if(ret) krb5_warn(context, ret, "kadm5_log_foreach"); ret = kadm5_log_end (server_context); @@ -400,6 +540,7 @@ iprop_replay(struct replay_options *opt, int argc, char **argv) if (ret) krb5_err (context, 1, ret, "db->close"); + kadm5_destroy(server_context); return 0; } diff --git a/lib/kadm5/iprop.8 b/lib/kadm5/iprop.8 index 6be1f110c..fd9f2f33c 100644 --- a/lib/kadm5/iprop.8 +++ b/lib/kadm5/iprop.8 @@ -38,7 +38,7 @@ .Nm iprop , .Nm ipropd-master , .Nm ipropd-slave -.Nd propagate changes to a Heimdal Kerberos master KDC to slave KDCs +.Nd propagate transactions from a Heimdal Kerberos master KDC to slave KDCs .Sh SYNOPSIS .Nm ipropd-master .Oo Fl c Ar string \*(Ba Xo @@ -110,13 +110,18 @@ which sends the whole database to the slaves regularly, .Nm normally sends only the changes as they happen on the master. The master keeps track of all the changes by assigning a version -number to every change to the database. +number to every transaction to the database. The slaves know which was the latest version they saw, and in this way it can be determined if they are in sync or not. -A log of all the changes is kept on the master. +A log of all the transactions is kept on the master. When a slave is at an older version than the oldest one in the log, the whole database has to be sent. .Pp +The log of transactions is also used to implement a two-phase commit +(with roll-forward for recovery) method of updating the HDB. +Transactions are first recorded in the log, then in the HDB, then +the log is updated to mark the transaction as committed. +.Pp The changes are propagated over a secure channel (on port 2121 by default). This should normally be defined as @@ -175,6 +180,11 @@ like 5 min, 300 s, or simply a number of seconds. .Pa slaves , .Pa slave-stats in the database directory. +.Pa ipropd-master.pid , +.Pa ipropd-slave.pid +in the database directory, or in the directory named by the +.Ev HEIM_PIDFILE_DIR +environment variable. .Sh SEE ALSO .Xr krb5.conf 5 , .Xr hprop 8 , diff --git a/lib/kadm5/ipropd_master.c b/lib/kadm5/ipropd_master.c index 168e394df..64d5e975d 100644 --- a/lib/kadm5/ipropd_master.c +++ b/lib/kadm5/ipropd_master.c @@ -125,6 +125,7 @@ struct slave { char *name; krb5_auth_context ac; uint32_t version; + int32_t version_tstamp; time_t seen; unsigned long flags; #define SLAVE_F_DEAD 0x1 @@ -356,6 +357,9 @@ write_dump (krb5_context context, krb5_storage *dump, if (ret) return ret; + if (krb5_storage_seek(dump, 0, SEEK_SET) != 0) + return errno; + /* * First we store zero as the HDB version, this will indicate to a * later reader that the dumpfile is invalid. We later write the @@ -454,13 +458,15 @@ write_dump (krb5_context context, krb5_storage *dump, static int send_complete (krb5_context context, slave *s, const char *database, - uint32_t current_version, uint32_t oldest_version) + uint32_t current_version, uint32_t oldest_version, + int32_t initial_log_tstamp) { krb5_error_code ret; krb5_storage *dump = NULL; uint32_t vno = 0; krb5_data data; int fd = -1; + struct stat st; char *dfn; ret = asprintf(&dfn, "%s/ipropd.dumpfile", hdb_db_dir(context)); @@ -506,12 +512,19 @@ send_complete (krb5_context context, slave *s, const char *database, goto done; } + if (fstat(fd, &st) == -1) { + ret = errno; + krb5_warn(context, ret, "send_complete: could not stat dump file"); + goto done; + } + /* * If the current dump has an appropriate version, then we can * break out of the loop and send the file below. */ - if (ret == 0 && vno >= oldest_version && vno <= current_version) + if (ret == 0 && vno != 0 && st.st_mtime > initial_log_tstamp && + vno >= oldest_version && vno <= current_version) break; /* @@ -546,8 +559,15 @@ send_complete (krb5_context context, slave *s, const char *database, goto done; } + if (fstat(fd, &st) == -1) { + ret = errno; + krb5_warn(context, ret, "write_dump: could not stat dump file"); + goto done; + } + /* check if someone wrote a better version for us */ - if (vno >= oldest_version) + if (ret == 0 && vno != 0 && st.st_mtime > initial_log_tstamp && + vno >= oldest_version && vno <= current_version) continue; /* Now, we know that we must write a new dump file. */ @@ -643,18 +663,26 @@ send_are_you_there (krb5_context context, slave *s) } static int -send_diffs (krb5_context context, slave *s, int log_fd, - const char *database, uint32_t current_version) +send_diffs (kadm5_server_context *server_context, slave *s, int log_fd, + const char *database, uint32_t current_version, + int32_t current_tstamp) { + krb5_context context = server_context->context; krb5_storage *sp; - uint32_t ver; - time_t timestamp; + uint32_t ver, initial_version, initial_version2; + int32_t initial_tstamp, initial_tstamp2; enum kadm_ops op; uint32_t len; off_t right, left; + krb5_ssize_t bytes; krb5_data data; int ret = 0; + if (s->flags & SLAVE_F_DEAD) { + krb5_warnx(context, "not sending diffs to a dead slave"); + return 0; + } + if (s->version == current_version) { char buf[4]; @@ -666,38 +694,94 @@ send_diffs (krb5_context context, slave *s, int log_fd, data.data = buf; data.length = 4; ret = krb5_write_priv_message(context, s->ac, &s->fd, &data); + if (ret) { + krb5_warn(context, ret, "send_diffs: failed to send to slave"); + slave_dead(context, s); + } krb5_warnx(context, "slave %s in sync already at version %ld", s->name, (long)s->version); return ret; } - if (s->flags & SLAVE_F_DEAD) - return 0; + krb5_warnx(context, "sending diffs to a live-seeming slave"); - flock(log_fd, LOCK_SH); - sp = kadm5_log_goto_end (log_fd); + /* + * XXX The code that makes the diffs should be made a separate function, + * then error handling (send_are_you_there() or slave_dead()) can be done + * here. + */ + + if (flock(log_fd, LOCK_SH) == -1) { + krb5_warn(context, errno, "could not obtain shared lock on log file"); + send_are_you_there(context, s); + return errno; + } + ret = kadm5_log_get_version_fd(server_context, log_fd, LOG_VERSION_FIRST, + &initial_version, &initial_tstamp); + sp = kadm5_log_goto_end(server_context, log_fd); flock(log_fd, LOCK_UN); + if (ret) { + if (sp != NULL) + krb5_storage_free(sp); + krb5_warn(context, ret, "send_diffs: failed to read log"); + send_are_you_there(context, s); + return ret; + } + if (sp == NULL) { + send_are_you_there(context, s); + krb5_warn(context, errno ? errno : EINVAL, + "send_diffs: failed to read log"); + return errno ? errno : EINVAL; + } + /* + * We're not holding any locks here, so we can't prevent truncations. + * + * We protect against this by re-checking that the initial version and + * timestamp are the same before and after this loop. + */ right = krb5_storage_seek(sp, 0, SEEK_CUR); + if (right == (off_t)-1) { + krb5_storage_free(sp); + send_are_you_there(context, s); + return errno; + } for (;;) { - ret = kadm5_log_previous (context, sp, &ver, ×tamp, &op, &len); + ret = kadm5_log_previous (context, sp, &ver, NULL, &op, &len); if (ret) krb5_err(context, 1, ret, "send_diffs: failed to find previous entry"); left = krb5_storage_seek(sp, -16, SEEK_CUR); - if (ver == s->version) - return 0; + if (left == (off_t)-1) { + krb5_storage_free(sp); + send_are_you_there(context, s); + return errno; + } if (ver == s->version + 1) break; + if (ver == s->version) { + /* + * This shouldn't happen, but recall we're not holding a lock on + * the log. + */ + krb5_storage_free(sp); + krb5_warnx(context, "iprop log truncated while sending diffs to " + "slave?? ver = %lu", (unsigned long)ver); + send_are_you_there(context, s); + return 0; + } if (left == 0) { krb5_storage_free(sp); krb5_warnx(context, "slave %s (version %lu) out of sync with master " "(first version in log %lu), sending complete database", s->name, (unsigned long)s->version, (unsigned long)ver); - return send_complete (context, s, database, current_version, ver); + return send_complete (context, s, database, current_version, ver, + initial_tstamp); } } + assert(ver == s->version + 1); + krb5_warnx(context, "syncing slave %s from version %lu to version %lu", s->name, (unsigned long)s->version, @@ -707,16 +791,49 @@ send_diffs (krb5_context context, slave *s, int log_fd, if (ret) { krb5_storage_free(sp); krb5_warn (context, ret, "send_diffs: krb5_data_alloc"); - slave_dead(context, s); + send_are_you_there(context, s); return 1; } - krb5_storage_read (sp, (char *)data.data + 4, data.length - 4); + bytes = krb5_storage_read(sp, (char *)data.data + 4, data.length - 4); krb5_storage_free(sp); + if (bytes != data.length - 4) { + krb5_warnx(context, "iprop log truncated while sending diffs to " + "slave?? ver = %lu", (unsigned long)ver); + send_are_you_there(context, s); + return 1; + } + + /* + * Check that we have the same log initial version and timestamp now as + * when we dropped the shared lock on the log file! Else we could be + * sending garbage to the slave. + */ + if (flock(log_fd, LOCK_SH) == -1) { + krb5_warn(context, errno, "could not obtain shared lock on log file"); + send_are_you_there(context, s); + return 1; + } + ret = kadm5_log_get_version_fd(server_context, log_fd, LOG_VERSION_FIRST, + &initial_version2, &initial_tstamp2); + flock(log_fd, LOCK_UN); + if (ret) { + krb5_warn(context, ret, + "send_diffs: failed to read log while producing diffs"); + send_are_you_there(context, s); + return 1; + } + if (initial_version != initial_version2 || + initial_tstamp != initial_tstamp2) { + krb5_warn(context, ret, + "send_diffs: log truncated while producing diffs"); + send_are_you_there(context, s); + return 1; + } sp = krb5_storage_from_data (&data); if (sp == NULL) { krb5_warnx (context, "send_diffs: krb5_storage_from_data"); - slave_dead(context, s); + send_are_you_there(context, s); return 1; } krb5_store_int32 (sp, FOR_YOU); @@ -734,13 +851,17 @@ send_diffs (krb5_context context, slave *s, int log_fd, s->version = current_version; + krb5_warnx(context, "slave is now up to date"); + return 0; } static int -process_msg (krb5_context context, slave *s, int log_fd, - const char *database, uint32_t current_version) +process_msg (kadm5_server_context *server_context, slave *s, int log_fd, + const char *database, uint32_t current_version, + int32_t current_tstamp) { + krb5_context context = server_context->context; int ret = 0; krb5_data out; krb5_storage *sp; @@ -785,8 +906,9 @@ process_msg (krb5_context context, slave *s, int log_fd, "version we already sent to it"); s->version = tmp; } - ret = send_diffs(context, s, log_fd, database, current_version); - break; + ret = send_diffs(server_context, s, log_fd, database, current_version, + current_tstamp); + break; case I_AM_HERE : break; case ARE_YOU_THERE: @@ -968,6 +1090,7 @@ main(int argc, char **argv) int log_fd; slave *slaves = NULL; uint32_t current_version = 0, old_version = 0; + int32_t current_tstamp = 0; krb5_keytab keytab; char **files; int aret; @@ -1053,8 +1176,11 @@ main(int argc, char **argv) signal_fd = make_signal_socket (context); listen_fd = make_listen_socket (context, port_str); - flock(log_fd, LOCK_SH); - kadm5_log_get_version_fd (log_fd, ¤t_version); + if (flock(log_fd, LOCK_SH) == -1) + krb5_err(context, 1, errno, "shared flock %s", + server_context->log_context.log_file); + kadm5_log_get_version_fd(server_context, log_fd, LOG_VERSION_LAST, + ¤t_version, ¤t_tstamp); flock(log_fd, LOCK_UN); krb5_warnx(context, "ipropd-master started at version: %lu", @@ -1096,10 +1222,42 @@ main(int argc, char **argv) krb5_err (context, 1, errno, "select"); } + if (stat(server_context->log_context.log_file, &st2) == -1) { + krb5_warn(context, errno, "could not stat log file by path"); + st2 = st; + } + + if (st2.st_dev != st.st_dev || st2.st_ino != st.st_ino) { + (void) close(log_fd); + + log_fd = open(server_context->log_context.log_file, O_RDONLY, 0); + if (log_fd < 0) + krb5_err(context, 1, 1, "open %s", + server_context->log_context.log_file); + + if (fstat(log_fd, &st) == -1) + krb5_err(context, 1, errno, "stat %s", + server_context->log_context.log_file); + + if (flock(log_fd, LOCK_SH) == -1) + krb5_err(context, 1, errno, "shared flock %s", + server_context->log_context.log_file); + kadm5_log_get_version_fd(server_context, log_fd, LOG_VERSION_LAST, + ¤t_version, ¤t_tstamp); + flock(log_fd, LOCK_UN); + } + if (ret == 0) { - old_version = current_version; - flock(log_fd, LOCK_SH); - kadm5_log_get_version_fd (log_fd, ¤t_version); + /* Recover from failed transactions */ + if (kadm5_log_init_nb(server_context) == 0) + kadm5_log_end(server_context); + + if (flock(log_fd, LOCK_SH) == -1) { + krb5_err(context, 1, errno, + "could not lock log file"); + } + kadm5_log_get_version_fd(server_context, log_fd, LOG_VERSION_LAST, + ¤t_version, ¤t_tstamp); flock(log_fd, LOCK_UN); if (current_version > old_version) { @@ -1110,7 +1268,8 @@ main(int argc, char **argv) for (p = slaves; p != NULL; p = p->next) { if (p->flags & SLAVE_F_DEAD) continue; - send_diffs (context, p, log_fd, database, current_version); + send_diffs (server_context, p, log_fd, database, + current_version, current_tstamp); } } } @@ -1131,10 +1290,25 @@ main(int argc, char **argv) --ret; assert(ret >= 0); old_version = current_version; - flock(log_fd, LOCK_SH); - kadm5_log_get_version_fd (log_fd, ¤t_version); + if (flock(log_fd, LOCK_SH) == -1) + krb5_err(context, 1, errno, "shared flock %s", + server_context->log_context.log_file); + kadm5_log_get_version_fd(server_context, log_fd, LOG_VERSION_LAST, + ¤t_version, ¤t_tstamp); flock(log_fd, LOCK_UN); - if (current_version > old_version) { + if (current_version != old_version) { + /* + * If current_version < old_version then the log got + * truncated and we'll end up doing full propagations. + * + * Truncating the log when the current version is + * numerically small can lead to race conditions. + * Ideally we should identify log versions as + * {init_or_trunc_time, vno}, then we could not have any + * such race conditions, but this would either require + * breaking backwards compatibility for the protocol or + * adding new messages to it. + */ krb5_warnx(context, "Got a signal, updating slaves %lu to %lu", (unsigned long)old_version, @@ -1142,7 +1316,8 @@ main(int argc, char **argv) for (p = slaves; p != NULL; p = p->next) { if (p->flags & SLAVE_F_DEAD) continue; - send_diffs (context, p, log_fd, database, current_version); + send_diffs (server_context, p, log_fd, database, + current_version, current_tstamp); } } else { krb5_warnx(context, @@ -1157,7 +1332,8 @@ main(int argc, char **argv) if (ret && FD_ISSET(p->fd, &readset)) { --ret; assert(ret >= 0); - if(process_msg (context, p, log_fd, database, current_version)) + if(process_msg (server_context, p, log_fd, database, + current_version, current_tstamp)) slave_dead(context, p); } else if (slave_gone_p (p)) slave_dead(context, p); diff --git a/lib/kadm5/ipropd_slave.c b/lib/kadm5/ipropd_slave.c index 73180acae..5c158faa9 100644 --- a/lib/kadm5/ipropd_slave.c +++ b/lib/kadm5/ipropd_slave.c @@ -176,97 +176,184 @@ ihave(krb5_context context, krb5_auth_context auth_context, return ret; } -static void +static int receive_loop (krb5_context context, krb5_storage *sp, kadm5_server_context *server_context) { int ret; - off_t left, right; + off_t left, right, off; + size_t mlen; void *buf; - int32_t vers, vers2; - ssize_t sret; + int32_t len, vers, vers2; + ssize_t sret, smlen; /* - * Seek to the current version of the local database. + * Seek to the first entry in the message from the master that is + * past the current version of the local database. */ do { - int32_t len, timestamp, tmp; + int32_t timestamp, tmp; - if(krb5_ret_int32 (sp, &vers) != 0) - return; - krb5_ret_int32 (sp, ×tamp); - krb5_ret_int32 (sp, &tmp); - krb5_ret_int32 (sp, &len); - if ((uint32_t)vers <= server_context->log_context.version) - krb5_storage_seek(sp, len + 8, SEEK_CUR); + if (krb5_ret_int32(sp, &vers) != 0 || + krb5_ret_int32(sp, ×tamp) != 0 || + krb5_ret_int32(sp, &tmp) != 0 || + krb5_ret_int32(sp, &len) != 0) { + + /* + * This shouldn't happen. Reconnecting probably won't help + * if it does happen, but by reconnecting we get a chance to + * connect to a new master if a new one is configured. + */ + krb5_warnx(context, "iprop entries from master were truncated"); + return EINVAL; + } + if (len < 0) { + krb5_warnx(context, "master sent entry with negative length for" + "version %ld", (long)vers); + return EINVAL; + } + if ((uint32_t)vers > server_context->log_context.version) + break; + off = krb5_storage_seek(sp, 0, SEEK_CUR); + if (krb5_storage_seek(sp, len + 8, SEEK_CUR) != off + len + 8) { + krb5_warnx(context, "iprop entries from master were truncated"); + return 0; + } } while((uint32_t)vers <= server_context->log_context.version); /* - * Read up rest of the entires into the memory... + * Read the remaining entries into memory... */ - left = krb5_storage_seek (sp, -16, SEEK_CUR); - right = krb5_storage_seek (sp, 0, SEEK_END); - buf = malloc (right - left); - if (buf == NULL && (right - left) != 0) - krb5_errx (context, 1, "malloc: no memory"); + /* SEEK_CUR is a header into the first entry we care about */ + left = krb5_storage_seek(sp, -16, SEEK_CUR); + right = krb5_storage_seek(sp, 0, SEEK_END); + if (right - left < 24 + len) { + krb5_warnx(context, "iprop entries from master were truncated"); + return EINVAL; + } + + mlen = (size_t)(right - left); + smlen = right - left; + buf = malloc (mlen); + if (buf == NULL && mlen != 0) { + krb5_warn(context, errno, "malloc: no memory"); + return ENOMEM; + } /* * ...and then write them out to the on-disk log. */ - krb5_storage_seek (sp, left, SEEK_SET); - krb5_storage_read (sp, buf, right - left); - sret = write (server_context->log_context.log_fd, buf, right-left); - if (sret != right - left) + /* NOTE: We haven't validated the entries yet */ + if (krb5_storage_seek(sp, left, SEEK_SET) != left) + krb5_errx(context, 1, "krb5_storage_seek() failed"); + sret = krb5_storage_read(sp, buf, mlen); + if (sret < 0) + return errno; + if (mlen != (size_t)sret) + krb5_errx(context, 1, "short krb5_storage_read() from memory buffer"); + sret = write(server_context->log_context.log_fd, buf, mlen); + if (sret != smlen) { + /* This is probably ENOSPC. We can't recover. */ krb5_err(context, 1, errno, "Failed to write log to disk"); - ret = fsync (server_context->log_context.log_fd); - if (ret) + } + ret = fsync(server_context->log_context.log_fd); + if (ret) { + /* This is also probably ENOSPC. We can't recover. */ krb5_err(context, 1, errno, "Failed to sync log to disk"); - free (buf); + } + free(buf); /* - * Go back to the startpoint and start to commit the entires to - * the database. + * Go back to the startpoint and commit the entries to the HDB. */ - krb5_storage_seek (sp, left, SEEK_SET); + krb5_storage_seek(sp, left, SEEK_SET); + ret = kadm5_log_recover(server_context, kadm_recover_replay); + if (ret) { + krb5_warn(context, ret, "replay of entries from master failed"); + return ret; + } - for(;;) { - int32_t len, len2, timestamp, tmp; + for (;;) { + int32_t len2, timestamp, tmp; off_t cur, cur2; enum kadm_ops op; - if(krb5_ret_int32 (sp, &vers) != 0) - break; - ret = krb5_ret_int32 (sp, ×tamp); - if (ret) krb5_errx(context, 1, "entry %ld: too short", (long)vers); - ret = krb5_ret_int32 (sp, &tmp); - if (ret) krb5_errx(context, 1, "entry %ld: too short", (long)vers); + if (krb5_ret_int32(sp, &vers) != 0) + break; + ret = krb5_ret_int32(sp, ×tamp); + if (ret) { + krb5_warnx(context, "entry %ld: too short", (long)vers); + return EINVAL; + } + ret = krb5_ret_int32(sp, &tmp); + if (ret) { + krb5_warnx(context, "entry %ld: too short", (long)vers); + return EINVAL; + } op = tmp; - ret = krb5_ret_int32 (sp, &len); - if (ret) krb5_errx(context, 1, "entry %ld: too short", (long)vers); - if (len < 0) - krb5_errx(context, 1, "log is corrupted, " - "negative length of entry version %ld: %ld", - (long)vers, (long)len); + ret = krb5_ret_int32(sp, &len); + if (ret) { + krb5_warnx(context, "entry %ld: too short", (long)vers); + return EINVAL; + } + if (len < 0) { + krb5_warnx(context, "entry %ld: negative length (%ld); " + "master is confused", (long)vers, (long)len); + return EINVAL; + } cur = krb5_storage_seek(sp, 0, SEEK_CUR); - krb5_warnx (context, "replaying entry %d", (int)vers); + krb5_warnx(context, "replaying entry %d", (int)vers); - ret = kadm5_log_replay (server_context, - op, vers, len, sp); + /* + * kadm5_log_replay() returns errors from among others, the HDB + * layer, which can return errors from the actual DBs, some of + * which return -1 and set errno, and some of which return + * system error codes. + */ + ret = kadm5_log_replay(server_context, + op, vers, len, sp); + if (ret == -1 && errno != 0) + ret = errno; if (ret) { - const char *s = krb5_get_error_message(server_context->context, ret); - krb5_warnx (context, - "kadm5_log_replay: %ld. Lost entry entry, " - "Database out of sync ?: %s (%d)", - (long)vers, s ? s : "unknown error", ret); + const char *s = krb5_get_error_message(server_context->context, ret); + + /* + * XXX We don't really know here whether the error is + * recoverable or not. Some HDB errors might be safe to + * ignore, and others will not be (e.g., any resulting from + * ENOSPC), but we can't tell which is which, particularly + * as errors from the databases are not mapped to HDB_ERR_*. + * + * We do our best to die if the error is not recoverable. + */ + switch (ret) { +#ifdef EDQUOT + case EDQUOT: +#endif + case ENOSPC: + case EPIPE: + case EINTR: + case EFBIG: + case EIO: + krb5_err(context, 1, ret, "kadm5_log_replay: %ld. Fatal write " + "error: %s (%d)", (long)vers, + s ? s : "unknown error", ret); + } + + krb5_warnx(context, + "kadm5_log_replay: %ld. Replay failed. " + "Database out of sync?: %s (%d)", + (long)vers, s ? s : "unknown error", ret); krb5_free_error_message(context, s); } { /* - * Make sure the krb5_log_replay does the right thing wrt - * reading out data from the sp. + * Make sure that kadm5_log_replay() read the whole entry + * from sp and left the sp offset at the start of the + * trailer. */ cur2 = krb5_storage_seek(sp, 0, SEEK_CUR); if (cur + len != cur2) @@ -275,59 +362,76 @@ receive_loop (krb5_context context, (long)vers); } - if (krb5_ret_int32 (sp, &len2) != 0) - krb5_errx(context, 1, "entry %ld: postamble too short", (long)vers); - if(krb5_ret_int32 (sp, &vers2) != 0) - krb5_errx(context, 1, "entry %ld: postamble too short", (long)vers); + if (krb5_ret_int32(sp, &len2) != 0) { + krb5_warnx(context, "entry %ld: postamble too short; " + "master is confused", (long)vers); + return EINVAL; + } + if(krb5_ret_int32(sp, &vers2) != 0) { + krb5_warnx(context, "entry %ld: postamble too short; " + "master is confused", (long)vers); + return EINVAL; + } + if (len != len2) { + krb5_warnx(context, "entry %ld: len != len2; master is " + "confused", (long)vers); + return EINVAL; + } + if (vers != vers2) { + krb5_warnx(context, "entry %ld: vers != vers2; master is " + "confused", (long)vers); + return EINVAL; + } - if (len != len2) - krb5_errx(context, 1, "entry %ld: len != len2", (long)vers); - if (vers != vers2) - krb5_errx(context, 1, "entry %ld: vers != vers2", (long)vers); + /* + * Update version after each replay. + */ + server_context->log_context.version = vers; + kadm5_log_update_uber(server_context); } - /* - * Update version - */ - - server_context->log_context.version = vers; + return 0; } -static void -receive (krb5_context context, - krb5_storage *sp, - kadm5_server_context *server_context) +static int +receive(krb5_context context, + krb5_storage *sp, + kadm5_server_context *server_context) { - int ret; + krb5_error_code ret, ret2; ret = server_context->db->hdb_open(context, server_context->db, O_RDWR | O_CREAT, 0600); if (ret) - krb5_err (context, 1, ret, "db->open"); + krb5_err(context, 1, ret, "db->open"); - receive_loop (context, sp, server_context); + ret2 = receive_loop(context, sp, server_context); + if (ret2) + krb5_warn(context, ret, "receive from ipropd-master had errors"); - ret = server_context->db->hdb_close (context, server_context->db); + ret = server_context->db->hdb_close(context, server_context->db); if (ret) - krb5_err (context, 1, ret, "db->close"); + krb5_err(context, 1, ret, "db->close"); + + return ret2; } static void -send_im_here (krb5_context context, int fd, - krb5_auth_context auth_context) +send_im_here(krb5_context context, int fd, + krb5_auth_context auth_context) { krb5_storage *sp; krb5_data data; - int ret; + krb5_error_code ret; - ret = krb5_data_alloc (&data, 4); + ret = krb5_data_alloc(&data, 4); if (ret) - krb5_err (context, 1, ret, "send_im_here"); + krb5_err(context, 1, ret, "send_im_here"); sp = krb5_storage_from_data (&data); if (sp == NULL) - krb5_errx (context, 1, "krb5_storage_from_data"); + krb5_errx(context, 1, "krb5_storage_from_data"); krb5_store_int32(sp, I_AM_HERE); krb5_storage_free(sp); @@ -335,7 +439,9 @@ send_im_here (krb5_context context, int fd, krb5_data_free(&data); if (ret) - krb5_err (context, 1, ret, "krb5_write_priv_message"); + krb5_err(context, 1, ret, "krb5_write_priv_message"); + + return; } static void @@ -347,15 +453,7 @@ reinit_log(krb5_context context, ret = kadm5_log_reinit(server_context); if (ret) - krb5_err(context, 1, ret, "kadm5_log_reinit"); - - ret = kadm5_log_set_version(server_context, vno - 1); - if (ret) - krb5_err(context, 1, ret, "kadm5_log_set_version"); - - ret = kadm5_log_nop(server_context); - if (ret) - krb5_err(context, 1, ret, "kadm5_log_nop"); + krb5_err(context, 1, ret, "kadm5_log_reinit"); } @@ -557,7 +655,7 @@ usage(int status) int main(int argc, char **argv) { - krb5_error_code ret; + krb5_error_code ret, ret2; krb5_context context; krb5_auth_context auth_context; void *kadm_handle; @@ -763,31 +861,56 @@ main(int argc, char **argv) krb5_err (context, 1, errno, "select"); } if (ret == 0) { - krb5_warn (context, 1, "server didn't send a message " - "in %d seconds", time_before_lost); + krb5_warnx(context, "server didn't send a message " + "in %d seconds", time_before_lost); connected = FALSE; continue; } ret = krb5_read_priv_message(context, auth_context, &master_fd, &out); if (ret) { - krb5_warn (context, ret, "krb5_read_priv_message"); + krb5_warn(context, ret, "krb5_read_priv_message"); connected = FALSE; continue; } sp = krb5_storage_from_mem (out.data, out.length); - krb5_ret_int32(sp, &tmp); + if (sp == NULL) + krb5_err(context, 1, errno, "krb5_storage_from_mem"); + ret = krb5_ret_int32(sp, &tmp); + if (ret == HEIM_ERR_EOF) { + krb5_warn(context, ret, "master sent zero-length message"); + connected = FALSE; + continue; + } + if (ret != 0) { + krb5_warn(context, ret, "couldn't read master's message"); + connected = FALSE; + continue; + } + + ret = kadm5_log_init(server_context); + if (ret) { + krb5_err(context, 1, ret, "kadm5_log_init while handling a " + "message from the master"); + } switch (tmp) { case FOR_YOU : - receive(context, sp, server_context); + ret2 = receive(context, sp, server_context); + if (ret2) + krb5_warn(context, ret, + "receive from ipropd-master had errors"); ret = ihave(context, auth_context, master_fd, server_context->log_context.version); - if (ret) + if (ret || ret2) connected = FALSE; - else - is_up_to_date(context, status_file, server_context); + /* + * If it returns an error, receive() may nonetheless + * have committed some entries successfully, so we must + * update the slave_status even if there were errors. + */ + is_up_to_date(context, status_file, server_context); break; case TELL_YOU_EVERYTHING : ret = receive_everything(context, master_fd, server_context, @@ -803,6 +926,11 @@ main(int argc, char **argv) break; case ARE_YOU_THERE : is_up_to_date(context, status_file, server_context); + ret = ihave(context, auth_context, master_fd, + server_context->log_context.version); + if (ret) + connected = FALSE; + send_im_here(context, master_fd, auth_context); break; case YOU_HAVE_LAST_VERSION: @@ -839,8 +967,10 @@ main(int argc, char **argv) } } - if (status_file) + if (status_file) { + /* XXX It'd be better to leave it saying we're not here */ unlink(status_file); + } if (0); #ifndef NO_SIGXCPU diff --git a/lib/kadm5/kadm5_err.et b/lib/kadm5/kadm5_err.et index f6343cbb2..0280a3dd6 100644 --- a/lib/kadm5/kadm5_err.et +++ b/lib/kadm5/kadm5_err.et @@ -65,3 +65,4 @@ error_code KEEPOLD_NOSUPP, "Keep old keys option not supported" error_code AUTH_GET_KEYS, "Operation requires `get-keys' privilege" error_code ALREADY_LOCKED, "Database already locked" error_code NOT_LOCKED, "Database not locked" +error_code LOG_CORRUPT, "Incremental propagation log got corrupted" diff --git a/lib/kadm5/libkadm5srv-exports.def b/lib/kadm5/libkadm5srv-exports.def index dca88da2d..ecffbb91c 100644 --- a/lib/kadm5/libkadm5srv-exports.def +++ b/lib/kadm5/libkadm5srv-exports.def @@ -59,6 +59,7 @@ EXPORTS kadm5_s_init_with_creds kadm5_s_chpass_principal_cond kadm5_log_set_version + kadm5_log_signal_master ;! kadm5_log_signal_socket kadm5_log_signal_socket_info ;! kadm5_log_previous @@ -66,10 +67,14 @@ EXPORTS kadm5_log_foreach kadm5_log_get_version_fd kadm5_log_get_version + kadm5_log_recover kadm5_log_replay kadm5_log_end kadm5_log_reinit kadm5_log_init + kadm5_log_init_nb + kadm5_log_init_nolock + kadm5_log_init_sharedlock kadm5_log_nop kadm5_log_truncate kadm5_log_modify diff --git a/lib/kadm5/log.c b/lib/kadm5/log.c index e31a30e43..34658ff6f 100644 --- a/lib/kadm5/log.c +++ b/lib/kadm5/log.c @@ -37,50 +37,479 @@ RCSID("$Id$"); /* - * A log record consists of: + * A log consists of a sequence of records of this form: * - * version number 4 bytes - * time in seconds 4 bytes - * operation (enum kadm_ops) 4 bytes - * length of record 4 bytes - * data... n bytes - * length of record 4 bytes - * version number 4 bytes + * version number 4 bytes -\ + * time in seconds 4 bytes +> preamble --+> header + * operation (enum kadm_ops) 4 bytes -/ / + * n, length of payload 4 bytes --------------+ + * PAYLOAD DATA... n bytes + * n, length of payload 4 bytes ----------------+> trailer + * version number 4 bytes ->postamble ---/ * + * I.e., records have a header and a trailer so that knowing the offset + * of an record's start or end one can traverse the log forwards and + * backwards. + * + * The log always starts with a nop record that contains the offset (8 + * bytes) where the next record should start after the last one, and the + * version number and timestamp of the last record: + * + * offset of next new record 8 bytes + * last record time 4 bytes + * last record version number 4 bytes + * + * kadm5 write operations are done in this order: + * + * - replay unconfirmed log records + * - write (append) and fsync() the log record for the kadm5 update + * - update the HDB (which includes fsync() or moral equivalent) + * - update the log uber record to mark the log record written as + * confirmed (not fsync()ed) + * + * This makes it possible and safe to seek to the logical end of the log + * (that is, the end of the last confirmed record) without traversing + * the whole log forward from offset zero. Unconfirmed records (which + * -currently- should never be more than one) can then be found (and + * rolled forward) by traversing forward from the logical end of the + * log. The trailers make it possible to traverse the log backwards + * from the logical end. + * + * This also makes the log + the HDB a two-phase commit with + * roll-forward system. + * + * HDB entry exists and HDB entry does not exist errors occurring during + * replay of unconfirmed records are ignored. This is because the + * corresponding HDB update might have completed. But also because a + * change to add aliases to a principal can fail because we don't check + * for alias conflicts before going ahead with the write operation. + * + * Non-sensical and incomplete log records found during roll-forward are + * truncated. A log record is non-sensical if its header and trailer + * don't match. + * + * Recovery (by rolling forward) occurs at the next read or write by a + * kadm5 API reader (e.g., kadmin), but not by an hdb API reader (e.g., + * the KDC). This means that, e.g., a principal rename could fail in + * between the store and the delete, and recovery might not take place + * until the next write operation. + * + * The log record payload format for create is: + * + * DER-encoded HDB_entry n bytes + * + * The log record payload format for update is: + * + * mask 4 bytes + * DER-encoded HDB_entry n-4 bytes + * + * The log record payload format for delete is: + * + * krb5_store_principal n bytes + * + * The log record payload format for rename is: + * + * krb5_store_principal m bytes (old principal name) + * DER-encoded HDB_entry n-m bytes (new record) + * + * The log record payload format for nop varies: + * + * - The zeroth record in new logs is a nop with a 16 byte payload: + * + * offset of end of last confirmed record 8 bytes + * version number of last confirmed record 4 bytes + * timestamp of last confirmed record 4 bytes + * + * - New non-zeroth nop records: + * + * nop type 4 bytes + * + * - Old nop records: + * + * version number 4 bytes + * timestamp 4 bytes + * + * Upon initialization, the log's uber record will have version 1, and + * will be followed by a nop record with version 2. The version numbers + * of additional records will be monotonically increasing. + * + * Truncation (kadm5_log_truncate()) takes some N > 0 records from the + * tail of the log and writes them to the beginning of the log after an + * uber record whose version will then be one less than the first of + * those records. + * + * On masters the log should never have more than one unconfirmed + * record, but slaves append all of a master's "diffs" and then call + * kadm5_log_recover() to recover. */ -kadm5_ret_t -kadm5_log_get_version_fd (int fd, - uint32_t *ver) -{ - int ret; - krb5_storage *sp; - int32_t old_version; +/* + * HDB and log lock order on the master: + * + * 1) open and lock the HDB + * 2) open and lock the log + * 3) do something + * 4) unlock and close the log + * 5) repeat (2)..(4) if desired + * 6) unlock and close the HDB + * + * The kadmin -l lock command can be used to hold the HDB open and + * locked for multiple operations. + * + * HDB and log lock order on the slave: + * + * 1) open and lock the log + * 2) open and lock the HDB + * 3) replay entries + * 4) unlock and close the HDB + * 5) repeat (2)..(4) until signaled + * 6) unlock and close the HDB + * + * The slave doesn't want to allow other local writers, after all, thus + * the order is reversed. This means that using "kadmin -l" on a slave + * will deadlock with ipropd-slave -- don't do that. + */ - ret = lseek (fd, 0, SEEK_END); - if(ret < 0) - return errno; - if(ret == 0) { - *ver = 0; - return 0; +#define LOG_HEADER_SZ (sizeof(uint32_t) * 4) +#define LOG_TRAILER_SZ (sizeof(uint32_t) * 2) +#define LOG_WRAPPER_SZ (LOG_HEADER_SZ + LOG_TRAILER_SZ) +#define LOG_UBER_SZ (sizeof(uint64_t) + sizeof(uint32_t) * 2) + +#define LOG_NOPEEK 0 +#define LOG_DOPEEK 1 + +/* + * Read the header of the record starting at the current offset into sp. + * + * Preserves sp's offset on success if `peek', else skips the header. + * + * Preserves sp's offset on failure where possible. + */ +static kadm5_ret_t +get_header(krb5_storage *sp, int peek, uint32_t *verp, int32_t *tstampp, + enum kadm_ops *opp, uint32_t *lenp) +{ + krb5_error_code ret; + uint32_t op, len; + int32_t tstamp; + off_t off, new_off; + + if (tstampp == NULL) + tstampp = &tstamp; + if (lenp == NULL) + lenp = &len; + + *verp = 0; + *tstampp = 0; + + off = krb5_storage_seek(sp, 0, SEEK_CUR); + if (off < 0) + return errno; + ret = krb5_ret_uint32(sp, verp); + if (ret == HEIM_ERR_EOF) { + (void) krb5_storage_seek(sp, off, SEEK_SET); + return HEIM_ERR_EOF; } - sp = krb5_storage_from_fd (fd); - krb5_storage_seek(sp, -4, SEEK_CUR); - krb5_ret_int32 (sp, &old_version); - *ver = old_version; - krb5_storage_free(sp); - lseek (fd, 0, SEEK_END); + if (ret) + goto log_corrupt; + ret = krb5_ret_int32(sp, tstampp); + if (ret) + goto log_corrupt; + + /* Note: sizeof(*opp) might not == sizeof(op) */ + ret = krb5_ret_uint32(sp, &op); + if (ret) + goto log_corrupt; + if (opp != NULL) + *opp = op; + + ret = krb5_ret_uint32(sp, lenp); + if (ret) + goto log_corrupt; + + /* Restore offset if requested */ + if (peek == LOG_DOPEEK) { + new_off = krb5_storage_seek(sp, off, SEEK_SET); + if (new_off == -1) + return errno; + if (new_off != off) + return EIO; + } + + return 0; + +log_corrupt: + (void) krb5_storage_seek(sp, off, SEEK_SET); + return KADM5_LOG_CORRUPT; +} + +/* + * Seek to the start of the preceding record's header and returns its + * offset. If sp is at offset zero this sets *verp = 0 and returns 0. + * + * Does not verify the header of the previous entry. + * + * On error returns -1, setting errno (possibly to a kadm5_ret_t or + * krb5_error_code value) and preserves sp's offset where possible. + */ +static off_t +seek_prev(krb5_storage *sp, uint32_t *verp, uint32_t *lenp) +{ + krb5_error_code ret; + uint32_t len, ver; + off_t off_len; + off_t off, new_off; + + if (lenp == NULL) + lenp = &len; + if (verp == NULL) + verp = &ver; + + *verp = 0; + *lenp = 0; + + off = krb5_storage_seek(sp, 0, SEEK_CUR); + if (off < 0) + return off; + if (off == 0) + return 0; + + /* Check that `off' allows for the record's header and trailer */ + if (off < LOG_WRAPPER_SZ) + goto log_corrupt; + + /* Get the previous entry's length and version from its trailer */ + new_off = krb5_storage_seek(sp, -8, SEEK_CUR); + if (new_off == -1) + return -1; + if (new_off != off - 8) { + errno = EIO; + return -1; + } + ret = krb5_ret_uint32(sp, lenp); + if (ret) + goto log_corrupt; + + /* Check for overflow/sign extension */ + off_len = (off_t)*lenp; + if (off_len < 0 || *lenp != (uint32_t)off_len) + goto log_corrupt; + + ret = krb5_ret_uint32(sp, verp); + if (ret) + goto log_corrupt; + + /* Check that `off' allows for the record */ + if (off < LOG_WRAPPER_SZ + off_len) + goto log_corrupt; + + /* Seek backwards to the entry's start */ + new_off = krb5_storage_seek(sp, -(LOG_WRAPPER_SZ + off_len), SEEK_CUR); + if (new_off == -1) + return -1; + if (new_off != off - (LOG_WRAPPER_SZ + off_len)) { + errno = EIO; + return -1; + } + return new_off; + +log_corrupt: + (void) krb5_storage_seek(sp, off, SEEK_SET); + errno = KADM5_LOG_CORRUPT; + return -1; +} + +/* + * Seek to the start of the next entry's header. + * + * On error returns -1 and preserves sp's offset. + */ +static off_t +seek_next(krb5_storage *sp) +{ + krb5_error_code ret; + uint32_t ver, ver2, len, len2; + enum kadm_ops op; + int32_t tstamp; + off_t off, off_len, new_off; + + off = krb5_storage_seek(sp, 0, SEEK_CUR); + if (off < 0) + return off; + + errno = get_header(sp, LOG_NOPEEK, &ver, &tstamp, &op, &len); + if (errno) + return -1; + + /* Check for overflow */ + off_len = len; + if (off_len < 0) + goto log_corrupt; + + new_off = krb5_storage_seek(sp, off_len, SEEK_CUR); + if (new_off == -1) { + (void) krb5_storage_seek(sp, off, SEEK_SET); + return -1; + } + if (new_off != off + LOG_HEADER_SZ + off_len) + goto log_corrupt; + ret = krb5_ret_uint32(sp, &len2); + if (ret || len2 != len) + goto log_corrupt; + ret = krb5_ret_uint32(sp, &ver2); + if (ret || ver2 != ver) + goto log_corrupt; + new_off = krb5_storage_seek(sp, 0, SEEK_CUR); + if (new_off == -1) { + (void) krb5_storage_seek(sp, off, SEEK_SET); + return -1; + } + if (new_off != off + off_len + LOG_WRAPPER_SZ) + goto log_corrupt; + + return off + off_len + LOG_WRAPPER_SZ; + +log_corrupt: + (void) krb5_storage_seek(sp, off, SEEK_SET); + errno = KADM5_LOG_CORRUPT; + return -1; +} + +/* + * Get the version of the entry ending at the current offset into sp. + * + * Returns HEIM_ERR_EOF if sp is at offset zero. + * + * Preserves sp's offset. + */ +static kadm5_ret_t +get_version_prev(krb5_storage *sp, uint32_t *verp, uint32_t *tstampp) +{ + krb5_error_code ret; + uint32_t ver2, len, len2; + off_t off, prev_off, new_off; + + *verp = 0; + if (tstampp != NULL) + *tstampp = 0; + + off = krb5_storage_seek(sp, 0, SEEK_CUR); + if (off < 0) + return errno; + if (off == 0) + return HEIM_ERR_EOF; + + /* Read the trailer and seek back */ + prev_off = seek_prev(sp, verp, &len); + if (prev_off == -1) + return errno; + + /* Verify that the trailer matches header */ + ret = get_header(sp, LOG_NOPEEK, &ver2, tstampp, NULL, &len2); + if (ret || *verp != ver2 || len != len2) + goto log_corrupt; + + /* Preserve offset */ + new_off = krb5_storage_seek(sp, off, SEEK_SET); + if (new_off == -1) + return errno; + if (new_off != off) { + errno = EIO; + return errno; + } + return 0; + +log_corrupt: + (void) krb5_storage_seek(sp, off, SEEK_SET); + return KADM5_LOG_CORRUPT; +} + +static size_t +get_max_log_size(krb5_context context) +{ + off_t n; + + /* Use database-label-specific lookup? No, ETOOHARD. */ + /* Default to 50MB max log size */ + n = krb5_config_get_int_default(context, NULL, 52428800, + "kdc", + "log-max-size", + NULL); + if (n >= 4 * (LOG_UBER_SZ + LOG_WRAPPER_SZ) && n == (size_t)n) + return (size_t)n; return 0; } +static kadm5_ret_t truncate_if_needed(kadm5_server_context *); +static krb5_storage *log_goto_first(kadm5_server_context *, int); + +/* + * Get the version and timestamp metadata of either the first, or last + * confirmed entry in the log. + * + * If `which' is LOG_VERSION_FIRST (1), then gets the metadata for the + * logically first entry past the uberblock. If `which' is + * LOG_VERSION_LAST (-1), then gets metadata for the last confirmed + * entry's version and timestpamp. + * + * The `fd''s offset will be set to the start of the header of the entry + * identified by `which'. + */ kadm5_ret_t -kadm5_log_get_version (kadm5_server_context *context, uint32_t *ver) +kadm5_log_get_version_fd(kadm5_server_context *server_context, int fd, + int which, uint32_t *ver, int32_t *tstamp) { - return kadm5_log_get_version_fd (context->log_context.log_fd, ver); + kadm5_ret_t ret; + krb5_storage *sp; + int32_t tmp; + + if (fd == -1) + return 0; /* /dev/null */ + + if (tstamp == NULL) + tstamp = &tmp; + + *ver = 0; + *tstamp = 0; + + switch (which) { + case LOG_VERSION_LAST: + sp = kadm5_log_goto_end(server_context, fd); + break; + case LOG_VERSION_FIRST: + sp = log_goto_first(server_context, fd); + break; + default: + return ENOTSUP; + } + + if (sp == NULL) + return errno; + + switch (which) { + case LOG_VERSION_LAST: + ret = get_version_prev(sp, ver, tstamp); + break; + case LOG_VERSION_FIRST: + ret = get_header(sp, LOG_DOPEEK, ver, tstamp, NULL, NULL); + break; + } + krb5_storage_free(sp); + return ret; } +/* Get the version of the last confirmed entry in the log */ kadm5_ret_t -kadm5_log_set_version (kadm5_server_context *context, uint32_t vno) +kadm5_log_get_version(kadm5_server_context *server_context, uint32_t *ver) +{ + return kadm5_log_get_version_fd(server_context, + server_context->log_context.log_fd, + LOG_VERSION_LAST, ver, NULL); +} + +/* Sets the version in the context, but NOT in the log */ +kadm5_ret_t +kadm5_log_set_version(kadm5_server_context *context, uint32_t vno) { kadm5_log_context *log_context = &context->log_context; @@ -88,194 +517,392 @@ kadm5_log_set_version (kadm5_server_context *context, uint32_t vno) return 0; } -kadm5_ret_t -kadm5_log_init (kadm5_server_context *context) +/* + * Open the log and setup server_context->log_context + * + * XXX: This and its callers should be named after "open", not "init" + */ +static kadm5_ret_t +log_init(kadm5_server_context *server_context, int lock_mode) { - int fd; + int fd = -1; + int lock_it = 0; + int lock_nb = 0; + int oflags = O_RDWR; + struct stat st; kadm5_ret_t ret; - kadm5_log_context *log_context = &context->log_context; + kadm5_log_context *log_context = &server_context->log_context; - if (log_context->log_fd != -1) - return 0; - fd = open (log_context->log_file, O_RDWR | O_CREAT, 0600); - if (fd < 0) { - ret = errno; - krb5_set_error_message(context->context, ret, "kadm5_log_init: open %s", - log_context->log_file); - return ret; - } - if (flock (fd, LOCK_EX) < 0) { - ret = errno; - krb5_set_error_message(context->context, ret, "kadm5_log_init: flock %s", - log_context->log_file); - close (fd); - return errno; + if (lock_mode & LOCK_NB) { + lock_mode &= ~LOCK_NB; + lock_nb = LOCK_NB; } - ret = kadm5_log_get_version_fd (fd, &log_context->version); - if (ret) - return ret; + if (lock_mode == log_context->lock_mode && log_context->log_fd != -1) + return 0; - log_context->log_fd = fd; - return 0; -} - -kadm5_ret_t -kadm5_log_reinit (kadm5_server_context *context) -{ - int fd; - int ret; - kadm5_log_context *log_context = &context->log_context; + if (strcmp(log_context->log_file, "/dev/null") == 0) { + /* log_context->log_fd should be -1 here */ + return 0; + } if (log_context->log_fd != -1) { - flock (log_context->log_fd, LOCK_UN); - close (log_context->log_fd); - log_context->log_fd = -1; + /* Lock or change lock */ + fd = log_context->log_fd; + if (lseek(fd, 0, SEEK_SET) == -1) + return errno; + lock_it = (lock_mode != log_context->lock_mode); + } else { + /* Open and lock */ + if (lock_mode != LOCK_UN) + oflags |= O_CREAT; + fd = open(log_context->log_file, oflags, 0600); + if (fd < 0) { + ret = errno; + krb5_set_error_message(server_context->context, ret, + "kadm5_log_init: open %s", + log_context->log_file); + return ret; + } + lock_it = (lock_mode != LOCK_UN); } - fd = open (log_context->log_file, O_RDWR | O_CREAT, 0600); - if (fd < 0) - return errno; - if (flock (fd, LOCK_EX) < 0) { + if (lock_it && flock(fd, lock_mode | lock_nb) < 0) { ret = errno; - close (fd); - return ret; - } - if (ftruncate(fd, 0) < 0) { - ret = errno; - close(fd); + krb5_set_error_message(server_context->context, ret, + "kadm5_log_init: flock %s", + log_context->log_file); + if (fd != log_context->log_fd) + (void) close(fd); return ret; } + log_context->log_fd = fd; + log_context->lock_mode = lock_mode; + log_context->read_only = (lock_mode != LOCK_EX); + + if (!log_context->read_only) { + if (fstat(fd, &st) == -1) + return errno; + if (st.st_size == 0) { + /* Write first entry */ + log_context->version = 0; + ret = kadm5_log_nop(server_context, kadm_nop_plain); + return ret; /* no need to truncate_if_needed(): it's not */ + } + ret = kadm5_log_recover(server_context, kadm_recover_replay); + if (ret) + return ret; + } + + ret = kadm5_log_get_version_fd(server_context, fd, LOG_VERSION_LAST, + &log_context->version, NULL); + if (ret && ret != HEIM_ERR_EOF) + return ret; + + return truncate_if_needed(server_context); +} + +/* Open the log with an exclusive lock */ +kadm5_ret_t +kadm5_log_init(kadm5_server_context *server_context) +{ + return log_init(server_context, LOCK_EX); +} + +/* Open the log with an exclusive non-blocking lock */ +kadm5_ret_t +kadm5_log_init_nb(kadm5_server_context *server_context) +{ + return log_init(server_context, LOCK_EX | LOCK_NB); +} + +/* Open the log with no locks */ +kadm5_ret_t +kadm5_log_init_nolock(kadm5_server_context *server_context) +{ + return log_init(server_context, LOCK_UN); +} + +/* Open the log with a shared lock */ +kadm5_ret_t +kadm5_log_init_sharedlock(kadm5_server_context *server_context, int lock_flags) +{ + return log_init(server_context, LOCK_SH | lock_flags); +} + +/* + * Reinitialize the log and open it + */ +kadm5_ret_t +kadm5_log_reinit(kadm5_server_context *server_context) +{ + int ret; + kadm5_log_context *log_context = &server_context->log_context; + + ret = log_init(server_context, LOCK_EX); + if (ret) + return ret; + if (log_context->log_fd != -1) { + if (ftruncate(log_context->log_fd, 0) < 0) { + ret = errno; + return ret; + } + if (lseek(log_context->log_fd, 0, SEEK_SET) < 0) { + ret = errno; + return ret; + } + } log_context->version = 0; - log_context->log_fd = fd; + + /* Write uber entry */ + ret = kadm5_log_nop(server_context, kadm_nop_trunc); return 0; } - +/* Close the server_context->log_context. */ kadm5_ret_t -kadm5_log_end (kadm5_server_context *context) +kadm5_log_end(kadm5_server_context *server_context) { - kadm5_log_context *log_context = &context->log_context; + kadm5_log_context *log_context = &server_context->log_context; + kadm5_ret_t ret = 0; int fd = log_context->log_fd; - flock (fd, LOCK_UN); - close(fd); + if (fd != -1) { + if (log_context->lock_mode != LOCK_UN) { + if (flock(fd, LOCK_UN) == -1 && errno == EBADF) + ret = errno; + } + if (ret != EBADF && close(fd) == -1) + ret = errno; + } log_context->log_fd = -1; - return 0; + log_context->lock_mode = LOCK_UN; + return ret; } +/* + * Write the version, timestamp, and op for a new entry. + * + * Note that the sp should be a krb5_storage_emem(), not a file. + * + * On success the sp's offset will be where the length of the payload + * should be written. + */ static kadm5_ret_t -kadm5_log_preamble (kadm5_server_context *context, - krb5_storage *sp, - enum kadm_ops op) +kadm5_log_preamble(kadm5_server_context *context, + krb5_storage *sp, + enum kadm_ops op) { kadm5_log_context *log_context = &context->log_context; - kadm5_ret_t kadm_ret; + time_t now = time(NULL); + kadm5_ret_t ret; - kadm_ret = kadm5_log_init (context); - if (kadm_ret) - return kadm_ret; + ret = krb5_store_uint32(sp, log_context->version + 1); + if (ret) + return ret; + ret = krb5_store_uint32(sp, now); + if (ret) + return ret; + log_context->last_time = now; - krb5_store_int32 (sp, ++log_context->version); - krb5_store_int32 (sp, time(NULL)); - krb5_store_int32 (sp, op); - return 0; + if (op < kadm_first || op > kadm_last) + return ERANGE; + return krb5_store_uint32(sp, op); } +/* Writes the version part of the trailer */ static kadm5_ret_t -kadm5_log_postamble (kadm5_log_context *context, - krb5_storage *sp) +kadm5_log_postamble(kadm5_log_context *context, + krb5_storage *sp) { - krb5_store_int32 (sp, context->version); + krb5_store_uint32(sp, context->version + 1); return 0; } /* - * flush the log record in `sp'. + * Signal the ipropd-master about changes to the log. + */ +/* + * XXX Get rid of the ifdef by having a sockaddr in log_context in both + * cases. + * + * XXX Better yet, just connect to the master's socket that slaves + * connect to, and then disconnect. The master should then check the + * log on every connection accepted. Then we wouldn't need IPC to + * signal the master. + */ +void +kadm5_log_signal_master(kadm5_server_context *context) +{ + kadm5_log_context *log_context = &context->log_context; +#ifndef NO_UNIX_SOCKETS + sendto(log_context->socket_fd, + (void *)&log_context->version, + sizeof(log_context->version), + 0, + (struct sockaddr *)&log_context->socket_name, + sizeof(log_context->socket_name)); +#else + sendto(log_context->socket_fd, + (void *)&log_context->version, + sizeof(log_context->version), + 0, + log_context->socket_info->ai_addr, + log_context->socket_info->ai_addrlen); +#endif +} + +/* + * Write sp's contents (which must be a fully formed record, complete + * with header, payload, and trailer) to the log and fsync the log. + * + * Does not free sp. */ static kadm5_ret_t -kadm5_log_flush (kadm5_log_context *log_context, - krb5_storage *sp) +kadm5_log_flush(kadm5_server_context *context, krb5_storage *sp) { + kadm5_log_context *log_context = &context->log_context; + kadm5_ret_t ret; krb5_data data; size_t len; - ssize_t ret; + krb5_ssize_t bytes; + uint32_t new_ver, prev_ver; + off_t off, end; + + if (strcmp(log_context->log_file, "/dev/null") == 0) + return 0; + + if (log_context->read_only) + return EROFS; + + if (krb5_storage_seek(sp, 0, SEEK_SET) == -1) + return errno; + + ret = get_header(sp, LOG_DOPEEK, &new_ver, NULL, NULL, NULL); + if (ret) + return ret; + + ret = krb5_storage_to_data(sp, &data); + if (ret) + return ret; + + /* Abandon the emem storage reference */ + sp = krb5_storage_from_fd(log_context->log_fd); + if (sp == NULL) { + krb5_data_free(&data); + return ENOMEM; + } + + /* Check that we are at the end of the log and fail if not */ + off = krb5_storage_seek(sp, 0, SEEK_CUR); + if (off == -1) { + krb5_data_free(&data); + krb5_storage_free(sp); + return errno; + } + end = krb5_storage_seek(sp, 0, SEEK_END); + if (end == -1) { + krb5_data_free(&data); + krb5_storage_free(sp); + return errno; + } + if (end != off) { + krb5_data_free(&data); + krb5_storage_free(sp); + return KADM5_LOG_CORRUPT; + } + + /* Enforce monotonically incremented versioning of records */ + if (seek_prev(sp, &prev_ver, NULL) == -1 || + krb5_storage_seek(sp, end, SEEK_SET) == -1) { + ret = errno; + krb5_data_free(&data); + krb5_storage_free(sp); + return ret; + } + + if (prev_ver != log_context->version) + return EINVAL; /* Internal error, really; just a consistency check */ + + if (new_ver != prev_ver + 1) { + krb5_warnx(context->context, "refusing to write a log record " + "with non-monotonic version (new: %u, old: %u)", + new_ver, prev_ver); + return KADM5_LOG_CORRUPT; + } - krb5_storage_to_data(sp, &data); len = data.length; - ret = write (log_context->log_fd, data.data, len); - if (ret < 0 || (size_t)ret != len) { - krb5_data_free(&data); - return errno; - } - if (fsync (log_context->log_fd) < 0) { - krb5_data_free(&data); - return errno; - } - - /* - * Try to send a signal to any running `ipropd-master' - */ -#ifndef NO_UNIX_SOCKETS - sendto (log_context->socket_fd, - (void *)&log_context->version, - sizeof(log_context->version), - 0, - (struct sockaddr *)&log_context->socket_name, - sizeof(log_context->socket_name)); -#else - sendto (log_context->socket_fd, - (void *)&log_context->version, - sizeof(log_context->version), - 0, - log_context->socket_info->ai_addr, - log_context->socket_info->ai_addrlen); -#endif - + bytes = krb5_storage_write(sp, data.data, len); krb5_data_free(&data); + if (bytes < 0) { + krb5_storage_free(sp); + return errno; + } + if (bytes != (krb5_ssize_t)len) { + krb5_storage_free(sp); + return EIO; + } + + ret = krb5_storage_fsync(sp); + krb5_storage_free(sp); + if (ret) + return ret; + + log_context->version = new_ver; return 0; } /* - * Add a `create' operation to the log. + * Add a `create' operation to the log and perform the create against the HDB. */ - kadm5_ret_t -kadm5_log_create (kadm5_server_context *context, - hdb_entry *ent) +kadm5_log_create(kadm5_server_context *context, hdb_entry *entry) { krb5_storage *sp; kadm5_ret_t ret; krb5_data value; + hdb_entry_ex ent; kadm5_log_context *log_context = &context->log_context; - sp = krb5_storage_emem(); - ret = hdb_entry2value (context->context, ent, &value); - if (ret) { - krb5_storage_free(sp); - return ret; - } - ret = kadm5_log_preamble (context, sp, kadm_create); - if (ret) { - krb5_data_free (&value); - krb5_storage_free(sp); - return ret; - } - krb5_store_int32 (sp, value.length); - krb5_storage_write(sp, value.data, value.length); - krb5_store_int32 (sp, value.length); - krb5_data_free (&value); - ret = kadm5_log_postamble (log_context, sp); - if (ret) { - krb5_storage_free (sp); - return ret; - } - ret = kadm5_log_flush (log_context, sp); - krb5_storage_free (sp); + memset(&ent, 0, sizeof(ent)); + ent.ctx = 0; + ent.free_entry = 0; + ent.entry = *entry; + + /* + * Test for any conflicting entries before writing the log. If we commit + * to the log we'll end-up rolling forward on recovery, but that would be + * wrong if the initial create is rejected. + */ + ret = context->db->hdb_store(context->context, context->db, + HDB_F_PRECHECK, &ent); + if (ret == 0) + ret = hdb_entry2value(context->context, entry, &value); if (ret) - return ret; - ret = kadm5_log_end (context); + return ret; + sp = krb5_storage_emem(); + if (sp == NULL) + ret = ENOMEM; + if (ret == 0) + ret = kadm5_log_preamble(context, sp, kadm_create); + if (ret == 0) + ret = krb5_store_uint32(sp, value.length); + if (ret == 0) { + if (krb5_storage_write(sp, value.data, value.length) != + (krb5_ssize_t)value.length) + ret = errno; + } + if (ret == 0) + ret = krb5_store_uint32(sp, value.length); + if (ret == 0) + ret = kadm5_log_postamble(log_context, sp); + if (ret == 0) + ret = kadm5_log_flush(context, sp); + krb5_storage_free(sp); + krb5_data_free(&value); + if (ret == 0) + ret = kadm5_log_recover(context, kadm_recover_commit); return ret; } @@ -283,12 +910,11 @@ kadm5_log_create (kadm5_server_context *context, * Read the data of a create log record from `sp' and change the * database. */ - static kadm5_ret_t -kadm5_log_replay_create (kadm5_server_context *context, - uint32_t ver, - uint32_t len, - krb5_storage *sp) +kadm5_log_replay_create(kadm5_server_context *context, + uint32_t ver, + uint32_t len, + krb5_storage *sp) { krb5_error_code ret; krb5_data data; @@ -296,158 +922,222 @@ kadm5_log_replay_create (kadm5_server_context *context, memset(&ent, 0, sizeof(ent)); - ret = krb5_data_alloc (&data, len); + ret = krb5_data_alloc(&data, len); if (ret) { krb5_set_error_message(context->context, ret, "out of memory"); return ret; } - krb5_storage_read (sp, data.data, len); - ret = hdb_value2entry (context->context, &data, &ent.entry); + krb5_storage_read(sp, data.data, len); + ret = hdb_value2entry(context->context, &data, &ent.entry); krb5_data_free(&data); if (ret) { krb5_set_error_message(context->context, ret, - "Unmarshaling hdb entry failed"); + "Unmarshaling hdb entry in log failed, " + "version: %ld", (long)ver); return ret; } ret = context->db->hdb_store(context->context, context->db, 0, &ent); - hdb_free_entry (context->context, &ent); + hdb_free_entry(context->context, &ent); return ret; } /* * Add a `delete' operation to the log. */ - kadm5_ret_t -kadm5_log_delete (kadm5_server_context *context, - krb5_principal princ) +kadm5_log_delete(kadm5_server_context *context, + krb5_principal princ) { - krb5_storage *sp; kadm5_ret_t ret; - off_t off; - off_t len; kadm5_log_context *log_context = &context->log_context; + krb5_storage *sp; + uint32_t len = 0; /* So dumb compilers don't warn */ + off_t end_off = 0; /* Ditto; this allows de-indentation by two levels */ + off_t off; + ret = context->db->hdb_remove(context->context, context->db, + HDB_F_PRECHECK, princ); + if (ret) + return ret; sp = krb5_storage_emem(); if (sp == NULL) - return ENOMEM; - ret = kadm5_log_preamble (context, sp, kadm_delete); - if (ret) - goto out; - ret = krb5_store_int32 (sp, 0); - if (ret) - goto out; - off = krb5_storage_seek (sp, 0, SEEK_CUR); - ret = krb5_store_principal (sp, princ); - if (ret) - goto out; - len = krb5_storage_seek (sp, 0, SEEK_CUR) - off; - krb5_storage_seek(sp, -(len + 4), SEEK_CUR); - ret = krb5_store_int32 (sp, len); - if (ret) - goto out; - krb5_storage_seek(sp, len, SEEK_CUR); - ret = krb5_store_int32 (sp, len); - if (ret) - goto out; - ret = kadm5_log_postamble (log_context, sp); - if (ret) - goto out; - ret = kadm5_log_flush (log_context, sp); - if (ret) - goto out; - ret = kadm5_log_end (context); -out: - krb5_storage_free (sp); + ret = ENOMEM; + if (ret == 0) + ret = kadm5_log_preamble(context, sp, kadm_delete); + if (ret) { + krb5_storage_free(sp); + return ret; + } + + /* + * Write a 0 length which we overwrite once we know the length of + * the principal name payload. + */ + off = krb5_storage_seek(sp, 0, SEEK_CUR); + if (off == -1) + ret = errno; + if (ret == 0) + ret = krb5_store_uint32(sp, 0); + if (ret == 0) + ret = krb5_store_principal(sp, princ); + if (ret == 0) { + end_off = krb5_storage_seek(sp, 0, SEEK_CUR); + if (end_off == -1) + ret = errno; + else if (end_off < off) + ret = KADM5_LOG_CORRUPT; + } + if (ret == 0) { + /* We wrote sizeof(uint32_t) + payload length bytes */ + len = (uint32_t)(end_off - off); + if (end_off - off != len || len < sizeof(len)) + ret = KADM5_LOG_CORRUPT; + else + len -= sizeof(len); + } + if (ret == 0 && krb5_storage_seek(sp, off, SEEK_SET) == -1) + ret = errno; + if (ret == 0) + ret = krb5_store_uint32(sp, len); + if (ret == 0 && krb5_storage_seek(sp, end_off, SEEK_SET) == -1) + ret = errno; + if (ret == 0) + ret = krb5_store_uint32(sp, len); + if (ret == 0) + ret = kadm5_log_postamble(log_context, sp); + if (ret == 0) + ret = kadm5_log_flush(context, sp); + if (ret == 0) + ret = kadm5_log_recover(context, kadm_recover_commit); + krb5_storage_free(sp); return ret; } /* * Read a `delete' log operation from `sp' and apply it. */ - static kadm5_ret_t -kadm5_log_replay_delete (kadm5_server_context *context, - uint32_t ver, - uint32_t len, - krb5_storage *sp) +kadm5_log_replay_delete(kadm5_server_context *context, + uint32_t ver, uint32_t len, krb5_storage *sp) { krb5_error_code ret; krb5_principal principal; - ret = krb5_ret_principal (sp, &principal); + ret = krb5_ret_principal(sp, &principal); if (ret) { krb5_set_error_message(context->context, ret, "Failed to read deleted " "principal from log version: %ld", (long)ver); return ret; } - ret = context->db->hdb_remove(context->context, context->db, principal); - krb5_free_principal (context->context, principal); + ret = context->db->hdb_remove(context->context, context->db, 0, principal); + krb5_free_principal(context->context, principal); return ret; } +static kadm5_ret_t kadm5_log_replay_rename(kadm5_server_context *, + uint32_t, uint32_t, + krb5_storage *); + /* * Add a `rename' operation to the log. */ - kadm5_ret_t -kadm5_log_rename (kadm5_server_context *context, - krb5_principal source, - hdb_entry *ent) +kadm5_log_rename(kadm5_server_context *context, + krb5_principal source, + hdb_entry *entry) { krb5_storage *sp; kadm5_ret_t ret; + uint32_t len = 0; /* So dumb compilers don't warn */ + off_t end_off = 0; /* Ditto; this allows de-indentation by two levels */ off_t off; - off_t len; krb5_data value; + hdb_entry_ex ent; kadm5_log_context *log_context = &context->log_context; - krb5_data_zero(&value); + memset(&ent, 0, sizeof(ent)); + ent.ctx = 0; + ent.free_entry = 0; + ent.entry = *entry; + + /* + * Pre-check that the transaction will succeed. + * + * Note that rename doesn't work to swap a principal's canonical + * name with one of its aliases. To make that work would require + * adding an hdb_rename() method for renaming principals (there's an + * hdb_rename() method already, but for renaming the HDB), which is + * ETOOMUCHWORK for the time being. + */ + ret = context->db->hdb_store(context->context, context->db, + HDB_F_PRECHECK, &ent); + if (ret == 0) + ret = context->db->hdb_remove(context->context, context->db, + HDB_F_PRECHECK, source); + if (ret) + return ret; sp = krb5_storage_emem(); - ret = hdb_entry2value (context->context, ent, &value); - if (ret) - goto failed; + krb5_data_zero(&value); + if (sp == NULL) + ret = ENOMEM; + if (ret == 0) + ret = kadm5_log_preamble(context, sp, kadm_rename); + if (ret == 0) + ret = hdb_entry2value(context->context, entry, &value); + if (ret) { + krb5_data_free(&value); + krb5_storage_free(sp); + return ret; + } - ret = kadm5_log_preamble (context, sp, kadm_rename); - if (ret) - goto failed; - - ret = krb5_store_int32 (sp, 0); - if (ret) - goto failed; - off = krb5_storage_seek (sp, 0, SEEK_CUR); - ret = krb5_store_principal (sp, source); - if (ret) - goto failed; - - krb5_storage_write(sp, value.data, value.length); - len = krb5_storage_seek (sp, 0, SEEK_CUR) - off; - - krb5_storage_seek(sp, -(len + 4), SEEK_CUR); - ret = krb5_store_int32 (sp, len); - if (ret) - goto failed; - - krb5_storage_seek(sp, len, SEEK_CUR); - ret = krb5_store_int32 (sp, len); - if (ret) - goto failed; - - ret = kadm5_log_postamble (log_context, sp); - if (ret) - goto failed; - - ret = kadm5_log_flush (log_context, sp); - if (ret) - goto failed; - krb5_storage_free (sp); - krb5_data_free (&value); - - return kadm5_log_end (context); - -failed: + /* + * Write a zero length which we'll overwrite once we know the length of the + * payload. + */ + off = krb5_storage_seek(sp, 0, SEEK_CUR); + if (off == -1) + ret = errno; + if (ret == 0) + ret = krb5_store_uint32(sp, 0); + if (ret == 0) + ret = krb5_store_principal(sp, source); + if (ret == 0) { + errno = 0; + if (krb5_storage_write(sp, value.data, value.length) != + (krb5_ssize_t)value.length) + ret = errno ? errno : EIO; + } + if (ret == 0) { + end_off = krb5_storage_seek(sp, 0, SEEK_CUR); + if (end_off == -1) + ret = errno; + else if (end_off < off) + ret = KADM5_LOG_CORRUPT; + } + if (ret == 0) { + /* We wrote sizeof(uint32_t) + payload length bytes */ + len = (uint32_t)(end_off - off); + if (end_off - off != len || len < sizeof(len)) + ret = KADM5_LOG_CORRUPT; + else + len -= sizeof(len); + if (ret == 0 && krb5_storage_seek(sp, off, SEEK_SET) == -1) + ret = errno; + if (ret == 0) + ret = krb5_store_uint32(sp, len); + if (ret == 0 && krb5_storage_seek(sp, end_off, SEEK_SET) == -1) + ret = errno; + if (ret == 0) + ret = krb5_store_uint32(sp, len); + if (ret == 0) + ret = kadm5_log_postamble(log_context, sp); + if (ret == 0) + ret = kadm5_log_flush(context, sp); + if (ret == 0) + ret = kadm5_log_recover(context, kadm_recover_commit); + } krb5_data_free(&value); krb5_storage_free(sp); return ret; @@ -458,10 +1148,10 @@ failed: */ static kadm5_ret_t -kadm5_log_replay_rename (kadm5_server_context *context, - uint32_t ver, - uint32_t len, - krb5_storage *sp) +kadm5_log_replay_rename(kadm5_server_context *context, + uint32_t ver, + uint32_t len, + krb5_storage *sp) { krb5_error_code ret; krb5_principal source; @@ -473,7 +1163,7 @@ kadm5_log_replay_rename (kadm5_server_context *context, memset(&target_ent, 0, sizeof(target_ent)); off = krb5_storage_seek(sp, 0, SEEK_CUR); - ret = krb5_ret_principal (sp, &source); + ret = krb5_ret_principal(sp, &source); if (ret) { krb5_set_error_message(context->context, ret, "Failed to read renamed " "principal in log, version: %ld", (long)ver); @@ -481,79 +1171,90 @@ kadm5_log_replay_rename (kadm5_server_context *context, } princ_len = krb5_storage_seek(sp, 0, SEEK_CUR) - off; data_len = len - princ_len; - ret = krb5_data_alloc (&value, data_len); + ret = krb5_data_alloc(&value, data_len); if (ret) { krb5_free_principal (context->context, source); return ret; } - krb5_storage_read (sp, value.data, data_len); - ret = hdb_value2entry (context->context, &value, &target_ent.entry); + krb5_storage_read(sp, value.data, data_len); + ret = hdb_value2entry(context->context, &value, &target_ent.entry); krb5_data_free(&value); if (ret) { - krb5_free_principal (context->context, source); + krb5_free_principal(context->context, source); return ret; } - ret = context->db->hdb_store (context->context, context->db, - 0, &target_ent); - hdb_free_entry (context->context, &target_ent); + ret = context->db->hdb_store(context->context, context->db, + 0, &target_ent); + hdb_free_entry(context->context, &target_ent); if (ret) { - krb5_free_principal (context->context, source); + krb5_free_principal(context->context, source); return ret; } - ret = context->db->hdb_remove (context->context, context->db, source); - krb5_free_principal (context->context, source); + ret = context->db->hdb_remove(context->context, context->db, 0, source); + krb5_free_principal(context->context, source); + return ret; } - /* * Add a `modify' operation to the log. */ - kadm5_ret_t -kadm5_log_modify (kadm5_server_context *context, - hdb_entry *ent, - uint32_t mask) +kadm5_log_modify(kadm5_server_context *context, + hdb_entry *entry, + uint32_t mask) { krb5_storage *sp; kadm5_ret_t ret; krb5_data value; uint32_t len; + hdb_entry_ex ent; kadm5_log_context *log_context = &context->log_context; - krb5_data_zero(&value); + memset(&ent, 0, sizeof(ent)); + ent.ctx = 0; + ent.free_entry = 0; + ent.entry = *entry; + + ret = context->db->hdb_store(context->context, context->db, + HDB_F_PRECHECK | HDB_F_REPLACE, &ent); + if (ret) + return ret; sp = krb5_storage_emem(); - ret = hdb_entry2value (context->context, ent, &value); - if (ret) - goto failed; + krb5_data_zero(&value); + if (sp == NULL) + ret = ENOMEM; + if (ret == 0) + ret = hdb_entry2value(context->context, entry, &value); + if (ret) { + krb5_data_free(&value); + krb5_storage_free(sp); + return ret; + } - ret = kadm5_log_preamble (context, sp, kadm_modify); - if (ret) - goto failed; - - len = value.length + 4; - ret = krb5_store_int32 (sp, len); - if (ret) - goto failed; - ret = krb5_store_int32 (sp, mask); - if (ret) - goto failed; - krb5_storage_write (sp, value.data, value.length); - - ret = krb5_store_int32 (sp, len); - if (ret) - goto failed; - ret = kadm5_log_postamble (log_context, sp); - if (ret) - goto failed; - ret = kadm5_log_flush (log_context, sp); - if (ret) - goto failed; - krb5_data_free(&value); - krb5_storage_free (sp); - return kadm5_log_end (context); -failed: + len = value.length + sizeof(len); + if (value.length > len || len > INT32_MAX) + ret = E2BIG; + if (ret == 0) + ret = kadm5_log_preamble(context, sp, kadm_modify); + if (ret == 0) + ret = krb5_store_uint32(sp, len); + if (ret == 0) + ret = krb5_store_uint32(sp, mask); + if (ret == 0) { + if (krb5_storage_write(sp, value.data, value.length) != + (krb5_ssize_t)value.length) + ret = errno; + } + if (ret == 0) + ret = krb5_store_uint32(sp, len); + if (ret == 0) + ret = kadm5_log_postamble(log_context, sp); + if (ret == 0) + ret = kadm5_log_flush(context, sp); + if (ret == 0) + ret = kadm5_log_recover(context, kadm_recover_commit); krb5_data_free(&value); krb5_storage_free(sp); return ret; @@ -562,28 +1263,31 @@ failed: /* * Read a `modify' log operation from `sp' and apply it. */ - static kadm5_ret_t -kadm5_log_replay_modify (kadm5_server_context *context, - uint32_t ver, - uint32_t len, - krb5_storage *sp) +kadm5_log_replay_modify(kadm5_server_context *context, + uint32_t ver, + uint32_t len, + krb5_storage *sp) { krb5_error_code ret; - int32_t mask; + uint32_t mask; krb5_data value; hdb_entry_ex ent, log_ent; memset(&log_ent, 0, sizeof(log_ent)); - krb5_ret_int32 (sp, &mask); + krb5_ret_uint32(sp, &mask); len -= 4; ret = krb5_data_alloc (&value, len); if (ret) { krb5_set_error_message(context->context, ret, "out of memory"); return ret; } - krb5_storage_read (sp, value.data, len); + errno = 0; + if (krb5_storage_read (sp, value.data, len) != (krb5_ssize_t)len) { + ret = errno ? errno : EIO; + return ret; + } ret = hdb_value2entry (context->context, &value, &log_ent.entry); krb5_data_free(&value); if (ret) @@ -627,7 +1331,8 @@ kadm5_log_replay_modify (kadm5_server_context *context, } } if (mask & KADM5_LAST_PWD_CHANGE) { - krb5_warnx (context->context, "Unimplemented mask KADM5_LAST_PWD_CHANGE"); + krb5_warnx (context->context, + "Unimplemented mask KADM5_LAST_PWD_CHANGE"); } if (mask & KADM5_ATTRIBUTES) { ent.entry.flags = log_ent.entry.flags; @@ -667,13 +1372,14 @@ kadm5_log_replay_modify (kadm5_server_context *context, ent.entry.kvno = log_ent.entry.kvno; } if (mask & KADM5_MKVNO) { - krb5_warnx (context->context, "Unimplemented mask KADM5_KVNO"); + krb5_warnx(context->context, "Unimplemented mask KADM5_KVNO"); } if (mask & KADM5_AUX_ATTRIBUTES) { - krb5_warnx (context->context, "Unimplemented mask KADM5_AUX_ATTRIBUTES"); + krb5_warnx(context->context, + "Unimplemented mask KADM5_AUX_ATTRIBUTES"); } if (mask & KADM5_POLICY_CLR) { - krb5_warnx (context->context, "Unimplemented mask KADM5_POLICY_CLR"); + krb5_warnx(context->context, "Unimplemented mask KADM5_POLICY_CLR"); } if (mask & KADM5_MAX_RLIFE) { if (log_ent.entry.max_renew == NULL) { @@ -691,13 +1397,14 @@ kadm5_log_replay_modify (kadm5_server_context *context, } } if (mask & KADM5_LAST_SUCCESS) { - krb5_warnx (context->context, "Unimplemented mask KADM5_LAST_SUCCESS"); + krb5_warnx(context->context, "Unimplemented mask KADM5_LAST_SUCCESS"); } if (mask & KADM5_LAST_FAILED) { - krb5_warnx (context->context, "Unimplemented mask KADM5_LAST_FAILED"); + krb5_warnx(context->context, "Unimplemented mask KADM5_LAST_FAILED"); } if (mask & KADM5_FAIL_AUTH_COUNT) { - krb5_warnx (context->context, "Unimplemented mask KADM5_FAIL_AUTH_COUNT"); + krb5_warnx(context->context, + "Unimplemented mask KADM5_FAIL_AUTH_COUNT"); } if (mask & KADM5_KEY_DATA) { size_t num; @@ -756,179 +1463,642 @@ kadm5_log_replay_modify (kadm5_server_context *context, ret = context->db->hdb_store(context->context, context->db, HDB_F_REPLACE, &ent); out: - hdb_free_entry (context->context, &ent); - hdb_free_entry (context->context, &log_ent); + hdb_free_entry(context->context, &ent); + hdb_free_entry(context->context, &log_ent); + return ret; +} + +/* + * Update the first entry (which should be a `nop'), the "uber-entry". + */ +static kadm5_ret_t +log_update_uber(kadm5_server_context *context, off_t off) +{ + kadm5_log_context *log_context = &context->log_context; + kadm5_ret_t ret = 0; + krb5_storage *sp, *mem_sp; + krb5_data data; + uint32_t op, len; + ssize_t bytes; + + if (strcmp(log_context->log_file, "/dev/null") == 0) + return 0; + + if (log_context->read_only) + return EROFS; + + krb5_data_zero(&data); + + mem_sp = krb5_storage_emem(); + if (mem_sp == NULL) + return ENOMEM; + + sp = krb5_storage_from_fd(log_context->log_fd); + if (sp == NULL) { + krb5_storage_free(mem_sp); + return ENOMEM; + } + + /* Skip first entry's version and timestamp */ + if (krb5_storage_seek(sp, 8, SEEK_SET) == -1) { + ret = errno; + goto out; + } + + /* If the first entry is not a nop, there's nothing we can do here */ + ret = krb5_ret_uint32(sp, &op); + if (ret || op != kadm_nop) + goto out; + + /* If the first entry is not a 16-byte nop, ditto */ + ret = krb5_ret_uint32(sp, &len); + if (ret || len != LOG_UBER_SZ) + goto out; + + /* + * Try to make the writes here as close to atomic as possible: a + * single write() call. + */ + ret = krb5_store_uint64(mem_sp, off); + if (ret) + goto out; + ret = krb5_store_int32(mem_sp, log_context->last_time); + if (ret) + goto out; + ret = krb5_store_uint32(mem_sp, log_context->version); + if (ret) + goto out; + + krb5_storage_to_data(mem_sp, &data); + bytes = krb5_storage_write(sp, data.data, data.length); + if (bytes < 0) + ret = errno; + else if (bytes != data.length) + ret = EIO; + + /* + * We don't fsync() this write because we can recover if the write + * doesn't complete, though for now we don't have code for properly + * dealing with the offset not getting written completely. + * + * We should probably have two copies of the offset so we can use + * one copy to verify the other, and when they don't match we could + * traverse the whole log forwards, replaying just the last entry. + */ + +out: + if (ret == 0) + kadm5_log_signal_master(context); + krb5_data_free(&data); + krb5_storage_free(sp); + krb5_storage_free(mem_sp); + if (lseek(log_context->log_fd, off, SEEK_SET) == -1) + ret = ret ? ret : errno; + return ret; } /* * Add a `nop' operation to the log. Does not close the log. */ - kadm5_ret_t -kadm5_log_nop (kadm5_server_context *context) +kadm5_log_nop(kadm5_server_context *context, enum kadm_nop_type nop_type) { krb5_storage *sp; kadm5_ret_t ret; kadm5_log_context *log_context = &context->log_context; + off_t off; + + off = lseek(log_context->log_fd, 0, SEEK_CUR); + if (off == -1) + return errno; sp = krb5_storage_emem(); - ret = kadm5_log_preamble (context, sp, kadm_nop); - if (ret) { - krb5_storage_free (sp); - return ret; - } - krb5_store_int32 (sp, 0); - krb5_store_int32 (sp, 0); - ret = kadm5_log_postamble (log_context, sp); - if (ret) { - krb5_storage_free (sp); - return ret; - } - ret = kadm5_log_flush (log_context, sp); - krb5_storage_free (sp); + ret = kadm5_log_preamble(context, sp, kadm_nop); + if (ret) + goto out; + if (off == 0) { + /* + * First entry (uber-entry) gets room for offset of next new + * entry and time and version of last entry. + */ + ret = krb5_store_uint32(sp, LOG_UBER_SZ); + /* These get overwritten with the same values below */ + if (ret == 0) + ret = krb5_store_uint64(sp, LOG_WRAPPER_SZ + LOG_UBER_SZ); + if (ret == 0) + ret = krb5_store_uint32(sp, log_context->last_time); + if (ret == 0) + ret = krb5_store_uint32(sp, log_context->version); + if (ret == 0) + ret = krb5_store_uint32(sp, LOG_UBER_SZ); + } else if (nop_type == kadm_nop_plain) { + ret = krb5_store_uint32(sp, 0); + if (ret == 0) + ret = krb5_store_uint32(sp, 0); + } else { + ret = krb5_store_uint32(sp, sizeof(uint32_t)); + if (ret == 0) + ret = krb5_store_uint32(sp, nop_type); + if (ret == 0) + ret = krb5_store_uint32(sp, sizeof(uint32_t)); + } + + if (ret == 0) + ret = kadm5_log_postamble(log_context, sp); + if (ret == 0) + ret = kadm5_log_flush(context, sp); + + if (ret == 0 && off == 0 && nop_type != kadm_nop_plain) + ret = kadm5_log_nop(context, nop_type); /* shouldn't happen */ + + if (ret == 0 && off != 0) + ret = kadm5_log_recover(context, kadm_recover_commit); + +out: + krb5_storage_free(sp); return ret; } /* - * Read a `nop' log operation from `sp' and apply it. + * Read a `nop' log operation from `sp' and "apply" it (there's nothing + * to do). + * + * FIXME Actually, if the nop payload is 4 bytes and contains an enum + * kadm_nop_type value of kadm_nop_trunc then we should truncate the + * log, and if it contains a kadm_nop_close then we should rename a new + * log into place. However, this is not implemented yet. */ - static kadm5_ret_t -kadm5_log_replay_nop (kadm5_server_context *context, - uint32_t ver, - uint32_t len, - krb5_storage *sp) +kadm5_log_replay_nop(kadm5_server_context *context, + uint32_t ver, + uint32_t len, + krb5_storage *sp) { return 0; } +struct replay_cb_data { + size_t count; + uint32_t ver; + enum kadm_recover_mode mode; +}; + + /* - * Call `func' for each log record in the log in `context' + * Recover or perform the initial commit of an unconfirmed log entry */ +static kadm5_ret_t +recover_replay(kadm5_server_context *context, + uint32_t ver, time_t timestamp, enum kadm_ops op, + uint32_t len, krb5_storage *sp, void *ctx) +{ + struct replay_cb_data *data = ctx; + kadm5_ret_t ret; + off_t off; + + /* On initial commit there must be just one pending unconfirmed entry */ + if (data->count > 0 && data->mode == kadm_recover_commit) + return KADM5_LOG_CORRUPT; + + /* We're at the start of the payload; compute end of entry offset */ + off = krb5_storage_seek(sp, 0, SEEK_CUR) + len + LOG_TRAILER_SZ; + + ret = kadm5_log_replay(context, op, ver, len, sp); + switch (ret) { + case HDB_ERR_NOENTRY: + case HDB_ERR_EXISTS: + if (data->mode != kadm_recover_replay) + return ret; + case 0: + break; + case KADM5_LOG_CORRUPT: + return -1; + default: + krb5_warn(context->context, ret, "unexpected error while replaying"); + return -1; + } + data->count++; + data->ver = ver; + + /* + * With replay we may be making multiple HDB changes. We must sync the + * confirmation of each one before moving on to the next. Otherwise, we + * might attempt to replay multiple already applied updates, and this may + * introduce unintended intermediate states or fail to yield the same final + * result. + */ + kadm5_log_set_version(context, ver); + ret = log_update_uber(context, off); + if (ret == 0 && data->mode != kadm_recover_commit) + ret = krb5_storage_fsync(sp); + return ret; +} + kadm5_ret_t -kadm5_log_foreach (kadm5_server_context *context, - void (*func)(kadm5_server_context *server_context, - uint32_t ver, - time_t timestamp, - enum kadm_ops op, - uint32_t len, - krb5_storage *, - void *), - void *ctx) +kadm5_log_recover(kadm5_server_context *context, enum kadm_recover_mode mode) { + kadm5_ret_t ret; + krb5_storage *sp; + struct replay_cb_data replay_data; + + replay_data.count = 0; + replay_data.ver = 0; + replay_data.mode = mode; + + sp = kadm5_log_goto_end(context, context->log_context.log_fd); + if (sp == NULL) + return errno ? errno : EIO; + + ret = kadm5_log_foreach(context, kadm_forward | kadm_unconfirmed, + NULL, recover_replay, &replay_data); + if (ret == 0 && mode == kadm_recover_commit && replay_data.count != 1) + ret = KADM5_LOG_CORRUPT; + krb5_storage_free(sp); + return ret; +} + +/* + * Call `func' for each log record in the log in `context'. + * + * `func' is optional. + * + * If `func' returns -1 then log traversal terminates and this returns 0. + * Otherwise `func''s return is returned if there are no other errors. + */ +kadm5_ret_t +kadm5_log_foreach(kadm5_server_context *context, + enum kadm_iter_opts iter_opts, + off_t *off_lastp, + kadm5_ret_t (*func)(kadm5_server_context *server_context, + uint32_t ver, time_t timestamp, + enum kadm_ops op, uint32_t len, + krb5_storage *sp, void *ctx), + void *ctx) +{ + kadm5_ret_t ret = 0; int fd = context->log_context.log_fd; krb5_storage *sp; + off_t off_last; + off_t this_entry = 0; + off_t log_end = 0; + + if (strcmp(context->log_context.log_file, "/dev/null") == 0) + return 0; + + if (off_lastp == NULL) + off_lastp = &off_last; + *off_lastp = -1; + + if (((iter_opts & kadm_forward) && (iter_opts & kadm_backward)) || + (!(iter_opts & kadm_confirmed) && !(iter_opts & kadm_unconfirmed))) + return EINVAL; + + if ((iter_opts & kadm_forward) && (iter_opts & kadm_confirmed) && + (iter_opts & kadm_unconfirmed)) { + /* + * We want to traverse all log entries, confirmed or not, from + * the start, then there's no need to kadm5_log_goto_end() + * -- no reason to try to find the end. + */ + sp = krb5_storage_from_fd(fd); + if (sp == NULL) + return errno; + + log_end = krb5_storage_seek(sp, 0, SEEK_END); + if (log_end == -1 || + krb5_storage_seek(sp, 0, SEEK_SET) == -1) { + ret = errno; + krb5_storage_free(sp); + return ret; + } + } else { + /* Get the end of the log based on the uber entry */ + sp = kadm5_log_goto_end(context, fd); + if (sp == NULL) + return errno; + log_end = krb5_storage_seek(sp, 0, SEEK_CUR); + } + + *off_lastp = log_end; + + if ((iter_opts & kadm_forward) && (iter_opts & kadm_confirmed)) { + /* Start at the beginning */ + if (krb5_storage_seek(sp, 0, SEEK_SET) == -1) { + ret = errno; + krb5_storage_free(sp); + return ret; + } + } else if ((iter_opts & kadm_backward) && (iter_opts & kadm_unconfirmed)) { + /* + * We're at the confirmed end but need to be at the unconfirmed + * end. Skip forward to the real end, re-entering to do it. + */ + ret = kadm5_log_foreach(context, kadm_forward | kadm_unconfirmed, + &log_end, NULL, NULL); + if (ret) + return ret; + if (krb5_storage_seek(sp, log_end, SEEK_SET) == -1) { + ret = errno; + krb5_storage_free(sp); + return ret; + } + } - lseek (fd, 0, SEEK_SET); - sp = krb5_storage_from_fd (fd); for (;;) { - int32_t ver, timestamp, op, len, len2, ver2; + uint32_t ver, ver2, len, len2; + int32_t tstamp; + time_t timestamp; + enum kadm_ops op; - if(krb5_ret_int32 (sp, &ver) != 0) + if ((iter_opts & kadm_backward)) { + off_t o; + + o = krb5_storage_seek(sp, 0, SEEK_CUR); + if (o == 0 || + ((iter_opts & kadm_unconfirmed) && o <= *off_lastp)) + break; + ret = kadm5_log_previous(context->context, sp, &ver, + ×tamp, &op, &len); + if (ret) + break; + + /* Offset is now at payload of current entry */ + + o = krb5_storage_seek(sp, 0, SEEK_CUR); + if (o == -1) { + ret = errno; + break; + } + this_entry = o - LOG_HEADER_SZ; + if (this_entry < 0) { + ret = KADM5_LOG_CORRUPT; + break; + } + } else { + /* Offset is now at start of current entry, read header */ + this_entry = krb5_storage_seek(sp, 0, SEEK_CUR); + if (!(iter_opts & kadm_unconfirmed) && this_entry == log_end) + break; + ret = get_header(sp, LOG_NOPEEK, &ver, &tstamp, &op, &len); + if (ret == HEIM_ERR_EOF) { + ret = 0; + break; + } + timestamp = tstamp; + if (ret) + break; + /* Offset is now at payload of current entry */ + } + + /* Validate trailer before calling the callback */ + if (krb5_storage_seek(sp, len, SEEK_CUR) == -1) { + ret = errno; + break; + } + + ret = krb5_ret_uint32(sp, &len2); + if (ret) + break; + ret = krb5_ret_uint32(sp, &ver2); + if (ret) + break; + if (len != len2 || ver != ver2) { + ret = KADM5_LOG_CORRUPT; break; - krb5_ret_int32 (sp, ×tamp); - krb5_ret_int32 (sp, &op); - krb5_ret_int32 (sp, &len); - (*func)(context, ver, timestamp, op, len, sp, ctx); - krb5_ret_int32 (sp, &len2); - krb5_ret_int32 (sp, &ver2); - if (len != len2) - abort(); - if (ver != ver2) - abort(); + } + + /* Rewind to start of payload and call callback if we have one */ + if (krb5_storage_seek(sp, this_entry + LOG_HEADER_SZ, + SEEK_SET) == -1) { + ret = errno; + break; + } + + if (func != NULL) { + ret = (*func)(context, ver, timestamp, op, len, sp, ctx); + if (ret) { + /* Callback signals desire to stop by returning -1 */ + if (ret == -1) + ret = 0; + break; + } + } + if ((iter_opts & kadm_forward)) { + off_t o; + + o = krb5_storage_seek(sp, this_entry+LOG_WRAPPER_SZ+len, SEEK_SET); + if (o == -1) { + ret = errno; + break; + } + if (o > log_end) + *off_lastp = o; + } else if ((iter_opts & kadm_backward)) { + /* + * Rewind to the start of this entry so kadm5_log_previous() + * can find the previous one. + */ + if (krb5_storage_seek(sp, this_entry, SEEK_SET) == -1) { + ret = errno; + break; + } + } + } + if ((ret == HEIM_ERR_EOF || ret == KADM5_LOG_CORRUPT) && + (iter_opts & kadm_forward) && + context->log_context.lock_mode == LOCK_EX) { + /* + * Truncate partially written last log entry so we can write + * again. + */ + ret = krb5_storage_truncate(sp, this_entry); + if (ret == 0 && + krb5_storage_seek(sp, this_entry, SEEK_SET) == -1) + ret = errno; + krb5_warnx(context->context, "Truncating log at partial or " + "corrupt %s entry", + this_entry > log_end ? "unconfirmed" : "confirmed"); } krb5_storage_free(sp); - return 0; + return ret; +} + +/* + * Go to the second record, which, if we have an uber record, will be + * the first record. + */ +static krb5_storage * +log_goto_first(kadm5_server_context *server_context, int fd) +{ + krb5_storage *sp; + enum kadm_ops op; + uint32_t ver, len; + kadm5_ret_t ret; + + if (fd == -1) { + errno = EINVAL; + return NULL; + } + + sp = krb5_storage_from_fd(fd); + if (sp == NULL) + return NULL; + + if (krb5_storage_seek(sp, 0, SEEK_SET) == -1) + return NULL; + + ret = get_header(sp, LOG_DOPEEK, &ver, NULL, &op, &len); + if (ret) { + krb5_storage_free(sp); + errno = ret; + return NULL; + } + if (op == kadm_nop && len == LOG_UBER_SZ && seek_next(sp) == -1) { + krb5_storage_free(sp); + return NULL; + } + return sp; } /* * Go to end of log. + * + * XXX This really needs to return a kadm5_ret_t and either output a + * krb5_storage * via an argument, or take one as input. */ krb5_storage * -kadm5_log_goto_end (int fd) +kadm5_log_goto_end(kadm5_server_context *server_context, int fd) { + krb5_error_code ret = 0; krb5_storage *sp; + enum kadm_ops op; + uint32_t ver, len; + int32_t tstamp; + uint64_t off; - sp = krb5_storage_from_fd (fd); - krb5_storage_seek(sp, 0, SEEK_END); + if (fd == -1) { + errno = EINVAL; + return NULL; + } + + sp = krb5_storage_from_fd(fd); + if (sp == NULL) + return NULL; + + if (krb5_storage_seek(sp, 0, SEEK_SET) == -1) { + ret = errno; + goto fail; + } + ret = get_header(sp, LOG_NOPEEK, &ver, &tstamp, &op, &len); + if (ret == HEIM_ERR_EOF) { + (void) krb5_storage_seek(sp, 0, SEEK_SET); + return sp; + } + if (ret == KADM5_LOG_CORRUPT) + goto truncate; + if (ret) + goto fail; + + if (op == kadm_nop && len == LOG_UBER_SZ) { + /* New style log */ + ret = krb5_ret_uint64(sp, &off); + if (ret) + goto truncate; + + if (krb5_storage_seek(sp, off, SEEK_SET) == -1) + goto fail; + + if (off >= LOG_WRAPPER_SZ + LOG_UBER_SZ) { + ret = get_version_prev(sp, &ver, NULL); + if (ret == 0) + return sp; + } + /* Invalid offset in uber entry */ + goto truncate; + } + + /* Old log with no uber entry */ + if (krb5_storage_seek(sp, 0, SEEK_END) == -1) { + static int warned = 0; + if (!warned) { + warned = 1; + krb5_warnx(server_context->context, + "Old log found; truncate it to upgrade"); + } + } + ret = get_version_prev(sp, &ver, NULL); + if (ret) + goto truncate; return sp; + +truncate: + /* If we can, truncate */ + if (server_context->log_context.lock_mode == LOCK_EX) { + ret = kadm5_log_reinit(server_context); + if (ret == 0) { + krb5_warn(server_context->context, ret, + "Invalid log; truncating to recover"); + if (krb5_storage_seek(sp, 0, SEEK_END) == -1) + return NULL; + return sp; + } + } + krb5_warn(server_context->context, ret, + "Invalid log; truncate to recover"); + +fail: + errno = ret; + krb5_storage_free(sp); + return NULL; } /* * Return previous log entry. * - * The pointer in `sp´ is assumed to be at the top of the entry before - * previous entry. On success, the `sp´ pointer is set to data portion - * of previous entry. In case of error, it's not changed at all. + * The pointer in `sp' is assumed to be at the top of the entry after + * previous entry (e.g., at EOF). On success, the `sp' pointer is set to + * data portion of previous entry. In case of error, it's not changed + * at all. */ - kadm5_ret_t -kadm5_log_previous (krb5_context context, - krb5_storage *sp, - uint32_t *ver, - time_t *timestamp, - enum kadm_ops *op, - uint32_t *len) +kadm5_log_previous(krb5_context context, + krb5_storage *sp, + uint32_t *verp, + time_t *tstampp, + enum kadm_ops *opp, + uint32_t *lenp) { krb5_error_code ret; - off_t off, oldoff; - int32_t tmp; + off_t oldoff; + uint32_t ver2, len2; + int32_t tstamp; oldoff = krb5_storage_seek(sp, 0, SEEK_CUR); + if (oldoff == -1) + goto log_corrupt; - krb5_storage_seek(sp, -8, SEEK_CUR); - ret = krb5_ret_int32 (sp, &tmp); - if (ret) - goto end_of_storage; - *len = tmp; - ret = krb5_ret_int32 (sp, &tmp); - if (ret) - goto end_of_storage; - *ver = tmp; - off = 24 + *len; - krb5_storage_seek(sp, -off, SEEK_CUR); - ret = krb5_ret_int32 (sp, &tmp); - if (ret) - goto end_of_storage; - if ((uint32_t)tmp != *ver) { - krb5_storage_seek(sp, oldoff, SEEK_SET); - krb5_set_error_message(context, KADM5_BAD_DB, - "kadm5_log_previous: log entry " - "have consistency failure, version number wrong " - "(tmp %lu ver %lu)", - (unsigned long)tmp, - (unsigned long)*ver); - return KADM5_BAD_DB; - } - ret = krb5_ret_int32 (sp, &tmp); - if (ret) - goto end_of_storage; - *timestamp = tmp; - ret = krb5_ret_int32 (sp, &tmp); - if (ret) - goto end_of_storage; - *op = tmp; - ret = krb5_ret_int32 (sp, &tmp); - if (ret) - goto end_of_storage; - if ((uint32_t)tmp != *len) { - krb5_storage_seek(sp, oldoff, SEEK_SET); - krb5_set_error_message(context, KADM5_BAD_DB, - "kadm5_log_previous: log entry " - "have consistency failure, length wrong"); - return KADM5_BAD_DB; + if (seek_prev(sp, verp, lenp) == -1) + goto log_corrupt; + + ret = get_header(sp, LOG_NOPEEK, &ver2, &tstamp, opp, &len2); + if (ret) { + (void) krb5_storage_seek(sp, oldoff, SEEK_SET); + return ret; } + if (tstampp) + *tstampp = tstamp; + if (ver2 != *verp || len2 != *lenp) + goto log_corrupt; + return 0; - end_of_storage: - krb5_storage_seek(sp, oldoff, SEEK_SET); - krb5_set_error_message(context, ret, "kadm5_log_previous: end of storage " - "reached before end"); - return ret; +log_corrupt: + (void) krb5_storage_seek(sp, oldoff, SEEK_SET); + return KADM5_LOG_CORRUPT; } /* @@ -936,65 +2106,365 @@ kadm5_log_previous (krb5_context context, */ kadm5_ret_t -kadm5_log_replay (kadm5_server_context *context, - enum kadm_ops op, - uint32_t ver, - uint32_t len, - krb5_storage *sp) +kadm5_log_replay(kadm5_server_context *context, + enum kadm_ops op, + uint32_t ver, + uint32_t len, + krb5_storage *sp) { switch (op) { case kadm_create : - return kadm5_log_replay_create (context, ver, len, sp); + return kadm5_log_replay_create(context, ver, len, sp); case kadm_delete : - return kadm5_log_replay_delete (context, ver, len, sp); + return kadm5_log_replay_delete(context, ver, len, sp); case kadm_rename : - return kadm5_log_replay_rename (context, ver, len, sp); + return kadm5_log_replay_rename(context, ver, len, sp); case kadm_modify : - return kadm5_log_replay_modify (context, ver, len, sp); + return kadm5_log_replay_modify(context, ver, len, sp); case kadm_nop : - return kadm5_log_replay_nop (context, ver, len, sp); + return kadm5_log_replay_nop(context, ver, len, sp); default : + /* + * FIXME This default arm makes it difficult to add new kadm_ops + * values. + */ krb5_set_error_message(context->context, KADM5_FAILURE, "Unsupported replay op %d", (int)op); + (void) krb5_storage_seek(sp, len, SEEK_CUR); return KADM5_FAILURE; } } -/* - * truncate the log - i.e. create an empty file with just (nop vno + 2) - */ +struct load_entries_data { + krb5_data *entries; + unsigned char *p; + uint32_t first; + size_t bytes; + size_t nentries; + size_t maxbytes; + size_t maxentries; +}; + +/* + * Prepend one entry with header and trailer to the entry buffer, stopping when + * we've reached either of the byte or entry-count limits (if non-zero). + * + * This is a two-pass algorithm: + * + * In the first pass, when entries->entries == NULL, we compute the space + * required, and count the entries that fit up from zero. + * + * In the second pass we fill the buffer, and count the entries back down to + * zero. The space used must be an exact fit, and the number of entries must + * reach zero at that point or an error is returned. + * + * The caller MUST check that entries->nentries == 0 at the end of the second + * pass. + */ +static kadm5_ret_t +load_entries_cb(kadm5_server_context *server_context, + uint32_t ver, + time_t timestamp, + enum kadm_ops op, + uint32_t len, + krb5_storage *sp, + void *ctx) +{ + struct load_entries_data *entries = ctx; + kadm5_ret_t ret; + ssize_t bytes; + size_t entry_len = len + LOG_WRAPPER_SZ; + unsigned char *base; + + if (entries->entries == NULL) { + size_t total = entries->bytes + entry_len; + + /* + * First run: find the size of krb5_data buffer needed. + * + * If the log was huge we'd have to perhaps open a temp file for this. + * For now KISS. + */ + if (ver < 2 /*1=uber*/ || entry_len < len /*overflow?*/ || + (entries->maxbytes > 0 && total > entries->maxbytes) || + total < entries->bytes /*overflow?*/ || + (entries->maxentries > 0 && entries->nentries == entries->maxentries)) + return -1; /* stop iteration */ + entries->bytes = total; + entries->first = ver; + ++entries->nentries; + return 0; + } + + /* Second run: load the data into memory */ + base = (unsigned char *)entries->entries->data; + if (entries->p - base < entry_len && entries->p != base) { + /* + * This can't happen normally: we stop the log record iteration + * above before we get here. This could happen if someone wrote + * garbage to the log while we were traversing it. We return an + * error instead of asserting. + */ + return KADM5_LOG_CORRUPT; + } + + /* + * sp here is a krb5_storage_from_fd() of the log file, and the + * offset pointer points at the current log record payload. + * + * Seek back to the start of the record poayload so we can read the + * whole record. + */ + if (krb5_storage_seek(sp, -LOG_HEADER_SZ, SEEK_CUR) == -1) + return errno; + + /* + * We read the header, payload, and trailer into the buffer we have, that + * many bytes before the previous record we read. + */ + errno = 0; + bytes = krb5_storage_read(sp, entries->p - entry_len, entry_len); + ret = errno; + if (bytes < 0 || bytes != entry_len) + return ret ? ret : EIO; + + entries->first = ver; + --entries->nentries; + entries->p -= entry_len; + return (entries->p == base) ? -1 : 0; +} + + +/* + * Serialize a tail fragment of the log as a krb5_data, this is constrained to + * at most `maxbytes' bytes and to at most `maxentries' entries if not zero. + */ +static kadm5_ret_t +load_entries(kadm5_server_context *context, krb5_data *p, + size_t maxentries, size_t maxbytes, uint32_t *first) +{ + struct load_entries_data entries; + kadm5_ret_t ret; + unsigned char *base; + + krb5_data_zero(p); + + *first = 0; + + memset(&entries, 0, sizeof(entries)); + entries.entries = NULL; + entries.p = NULL; + entries.maxentries = maxentries; + entries.maxbytes = maxbytes; + + /* Figure out how many bytes it will take */ + ret = kadm5_log_foreach(context, kadm_backward | kadm_confirmed, + NULL, load_entries_cb, &entries); + if (ret) + return ret; + + /* + * If no entries fit our limits, we do not truncate, instead the caller can + * call kadm5_log_reinit() if desired. + */ + if (entries.bytes == 0) + return 0; + + ret = krb5_data_alloc(p, entries.bytes); + if (ret) + return ret; + + *first = entries.first; + entries.entries = p; + base = (unsigned char *)entries.entries->data; + entries.p = base + entries.bytes; + + ret = kadm5_log_foreach(context, kadm_backward | kadm_confirmed, + NULL, load_entries_cb, &entries); + if (ret == 0 && + (entries.nentries || entries.p != base || entries.first != *first)) + ret = KADM5_LOG_CORRUPT; + if (ret) + krb5_data_free(p); + return ret; +} + +/* + * Truncate the log, retaining at most `keep' entries and at most `maxbytes'. + * If `maxbytes' is zero, keep at most the default log size limit. + */ kadm5_ret_t -kadm5_log_truncate (kadm5_server_context *server_context) +kadm5_log_truncate(kadm5_server_context *context, size_t keep, size_t maxbytes) { kadm5_ret_t ret; - uint32_t vno; + uint32_t first, last_tstamp; + time_t now = time(NULL); + krb5_data entries; + krb5_storage *sp; + ssize_t bytes; + uint64_t head_sz, sz; + off_t off; - ret = kadm5_log_init (server_context); + if (maxbytes == 0) + maxbytes = get_max_log_size(context->context); + + if (strcmp(context->log_context.log_file, "/dev/null") == 0) + return 0; + + if (context->log_context.read_only) + return EROFS; + + /* Get the desired records. */ + krb5_data_zero(&entries); + ret = load_entries(context, &entries, keep, maxbytes, &first); if (ret) - return ret; + return ret; - ret = kadm5_log_get_version (server_context, &vno); - if (ret) - return ret; + if (first == 0) { + /* + * No records found/fit within resource limits. The caller should call + * kadm5_log_reinit(context) to truly truncate and reset the log to + * version 1, else call again with better limits. + */ + krb5_data_free(&entries); + return EINVAL; + } - ret = kadm5_log_reinit (server_context); - if (ret) - return ret; + /* Size of the uber record */ + head_sz = LOG_WRAPPER_SZ + LOG_UBER_SZ; - ret = kadm5_log_set_version (server_context, vno); - if (ret) - return ret; + /* Check that entries.length won't overflow off_t */ + sz = head_sz + entries.length; + off = (off_t)sz; + if (off < 0 || off != sz || sz < entries.length) { + krb5_data_free(&entries); + return EOVERFLOW; /* caller should ask for fewer entries */ + } - ret = kadm5_log_nop (server_context); - if (ret) - return ret; + /* Truncate to zero size and seek to zero offset */ + if (ftruncate(context->log_context.log_fd, 0) < 0 || + lseek(context->log_context.log_fd, 0, SEEK_SET) < 0) { + krb5_data_free(&entries); + return errno; + } - ret = kadm5_log_end (server_context); - if (ret) - return ret; - return 0; + /* + * Write the uber record and then the records loaded. Confirm the entries + * after writing them. + * + * If we crash then the log may not have all the entries we want, and + * replaying only some of the entries will leave us in a bad state. + * Additionally, we don't have mathematical proof that replaying the last + * N>1 entries is always idempotent. And though we believe we can make + * such replays idempotent, they would still leave the HDB with + * intermediate states that would not have occurred on the master. + * + * By initially setting the offset in the uber record to 0, the log will be + * seen as invalid should we crash here, thus the only + * harm will be that we'll reinitialize the log and force full props. + * + * We can't use the normal kadm5_log_*() machinery for this because + * we must set specific version numbers and timestamps. To keep + * things simple we don't try to do a single atomic write here as we + * do in kadm5_log_flush(). + * + * We really do want to keep the new first entry's version and + * timestamp so we don't trip up iprop. + * + * Keep this in sync with kadm5_log_nop(). + */ + sp = krb5_storage_from_fd(context->log_context.log_fd); + if (sp == NULL) { + ret = errno; + krb5_warn(context->context, ret, "Unable to keep entries"); + krb5_data_free(&entries); + return errno; + } + ret = krb5_store_uint32(sp, first - 1); + if (ret == 0) + ret = krb5_store_uint32(sp, now); + if (ret == 0) + ret = krb5_store_uint32(sp, kadm_nop); /* end of preamble */ + if (ret == 0) + ret = krb5_store_uint32(sp, LOG_UBER_SZ); /* end of header */ + if (ret == 0) + ret = krb5_store_uint64(sp, 0); + if (ret == 0) + ret = krb5_store_uint32(sp, 0); + if (ret == 0) + ret = krb5_store_uint32(sp, 0); /* end of payload */ + if (ret == 0) + ret = krb5_store_uint32(sp, LOG_UBER_SZ); + if (ret == 0) + ret = krb5_store_uint32(sp, first - 1); /* end of trailer */ + if (ret == 0) { + bytes = krb5_storage_write(sp, entries.data, entries.length); + if (bytes == -1) + ret = errno; + } + if (ret == 0) + ret = krb5_storage_fsync(sp); + /* Confirm all the records now */ + if (ret == 0) { + if (krb5_storage_seek(sp, LOG_HEADER_SZ, SEEK_SET) == -1) + ret = errno; + } + if (ret == 0) + ret = krb5_store_uint64(sp, off); + krb5_data_free(&entries); + krb5_storage_free(sp); + if (ret) { + krb5_warn(context->context, ret, "Unable to keep entries"); + (void) ftruncate(context->log_context.log_fd, head_sz); + (void) lseek(context->log_context.log_fd, 0, SEEK_SET); + return ret; + } + + /* Done. Now rebuild the log_context state. */ + (void) lseek(context->log_context.log_fd, off, SEEK_SET); + sp = kadm5_log_goto_end(context, context->log_context.log_fd); + if (sp == NULL) + return ENOMEM; + ret = get_version_prev(sp, &context->log_context.version, &last_tstamp); + context->log_context.last_time = last_tstamp; + krb5_storage_free(sp); + return ret; +} + +/* + * "Truncate" the log if not read only and over the desired maximum size. We + * attempt to retain 1/4 of the existing storage. + * + * Called after successful log recovery, so at this point we must have no + * unconfirmed entries in the log. + */ +static kadm5_ret_t +truncate_if_needed(kadm5_server_context *context) +{ + kadm5_ret_t ret = 0; + kadm5_log_context *log_context = &context->log_context; + size_t maxbytes; + struct stat st; + + if (log_context->log_fd == -1 || log_context->read_only) + return 0; + if (strcmp(context->log_context.log_file, "/dev/null") == 0) + return 0; + + maxbytes = get_max_log_size(context->context); + if (maxbytes <= 0) + return 0; + + if (fstat(log_context->log_fd, &st) == -1) + return errno; + if (st.st_size == (size_t)st.st_size && (size_t)st.st_size <= maxbytes) + return 0; + + /* Shrink the log by a factor of 4 */ + ret = kadm5_log_truncate(context, 0, maxbytes/4); + return ret == EINVAL ? 0 : ret; } #ifndef NO_UNIX_SOCKETS diff --git a/lib/kadm5/modify_s.c b/lib/kadm5/modify_s.c index c810a5d90..8d92609f1 100644 --- a/lib/kadm5/modify_s.c +++ b/lib/kadm5/modify_s.c @@ -57,6 +57,11 @@ modify_principal(void *server_handle, if(ret) return ret; } + + ret = kadm5_log_init(context); + if (ret) + goto out; + ret = context->db->hdb_fetch_kvno(context->context, context->db, princ->principal, HDB_F_GET_ANY|HDB_F_ADMIN_DATA, 0, &ent); if(ret) @@ -105,20 +110,20 @@ modify_principal(void *server_handle, goto out2; } - ret = context->db->hdb_store(context->context, context->db, - HDB_F_REPLACE, &ent); - if (ret) - goto out2; - - kadm5_log_modify (context, - &ent.entry, - mask | KADM5_MOD_NAME | KADM5_MOD_TIME); + /* This logs the change for iprop and writes to the HDB */ + ret = kadm5_log_modify(context, &ent.entry, + mask | KADM5_MOD_NAME | KADM5_MOD_TIME); out2: hdb_free_entry(context->context, &ent); out: - if (!context->keep_open) - context->db->hdb_close(context->context, context->db); + (void) kadm5_log_end(context); + if (!context->keep_open) { + kadm5_ret_t ret2; + ret2 = context->db->hdb_close(context->context, context->db); + if (ret == 0 && ret2 != 0) + ret = ret2; + } return _kadm5_error_code(ret); } diff --git a/lib/kadm5/private.h b/lib/kadm5/private.h index 5a085eb39..13ec24533 100644 --- a/lib/kadm5/private.h +++ b/lib/kadm5/private.h @@ -82,7 +82,10 @@ typedef struct kadm5_log_peer { typedef struct kadm5_log_context { char *log_file; int log_fd; + int read_only; + int lock_mode; uint32_t version; + time_t last_time; #ifndef NO_UNIX_SOCKETS struct sockaddr_un socket_name; #else @@ -136,6 +139,11 @@ typedef struct kadm5_ad_context { char *base_dn; } kadm5_ad_context; +/* + * This enum is used in the iprop log file and on the wire in the iprop + * protocol. DO NOT CHANGE, except to add new op types at the end, and + * look for places in lib/kadm5/log.c to update. + */ enum kadm_ops { kadm_get, kadm_delete, @@ -147,7 +155,28 @@ enum kadm_ops { kadm_get_privs, kadm_get_princs, kadm_chpass_with_key, - kadm_nop + kadm_nop, + kadm_first = kadm_get, + kadm_last = kadm_nop +}; + +/* FIXME nop types are currently not implemented */ +enum kadm_nop_type { + kadm_nop_plain, /* plain nop, not relevance except as uberblock */ + kadm_nop_trunc, /* indicates that the master truncated the log */ + kadm_nop_close /* indicates that the master closed this log */ +}; + +enum kadm_iter_opts { + kadm_forward = 1, + kadm_backward = 2, + kadm_confirmed = 4, + kadm_unconfirmed = 8 +}; + +enum kadm_recover_mode { + kadm_recover_commit, + kadm_recover_replay }; #define KADMIN_APPL_VERSION "KADM0.1" diff --git a/lib/kadm5/randkey_s.c b/lib/kadm5/randkey_s.c index 336f3dfc1..e1308fd88 100644 --- a/lib/kadm5/randkey_s.c +++ b/lib/kadm5/randkey_s.c @@ -59,6 +59,11 @@ kadm5_s_randkey_principal(void *server_handle, if(ret) return ret; } + + ret = kadm5_log_init(context); + if (ret) + goto out; + ret = context->db->hdb_fetch_kvno(context->context, context->db, princ, HDB_F_GET_ANY|HDB_F_ADMIN_DATA, 0, &ent); if(ret) @@ -70,12 +75,8 @@ kadm5_s_randkey_principal(void *server_handle, goto out2; } - ret = _kadm5_set_keys_randomly (context, - &ent.entry, - n_ks_tuple, - ks_tuple, - new_keys, - n_keys); + ret = _kadm5_set_keys_randomly(context, &ent.entry, n_ks_tuple, ks_tuple, + new_keys, n_keys); if (ret) goto out2; ent.entry.kvno++; @@ -104,22 +105,19 @@ kadm5_s_randkey_principal(void *server_handle, hdb_replace_extension(context->context, &ent.entry, &ext); } - ret = context->db->hdb_store(context->context, context->db, - HDB_F_REPLACE, &ent); - if (ret) - goto out2; - - kadm5_log_modify(context, &ent.entry, - KADM5_ATTRIBUTES | KADM5_PRINCIPAL | KADM5_MOD_NAME | - KADM5_MOD_TIME | KADM5_KEY_DATA | KADM5_KVNO | - KADM5_PW_EXPIRATION | KADM5_TL_DATA); + /* This logs the change for iprop and writes to the HDB */ + ret = kadm5_log_modify(context, &ent.entry, + KADM5_ATTRIBUTES | KADM5_PRINCIPAL | + KADM5_MOD_NAME | KADM5_MOD_TIME | + KADM5_KEY_DATA | KADM5_KVNO | + KADM5_PW_EXPIRATION | KADM5_TL_DATA); out3: if (ret) { int i; for (i = 0; i < *n_keys; ++i) - krb5_free_keyblock_contents (context->context, &(*new_keys)[i]); + krb5_free_keyblock_contents(context->context, &(*new_keys)[i]); free (*new_keys); *new_keys = NULL; *n_keys = 0; @@ -127,7 +125,12 @@ out3: out2: hdb_free_entry(context->context, &ent); out: - if (!context->keep_open) - context->db->hdb_close(context->context, context->db); + (void) kadm5_log_end(context); + if (!context->keep_open) { + kadm5_ret_t ret2; + ret2 = context->db->hdb_close(context->context, context->db); + if (ret == 0 && ret2 != 0) + ret = ret2; + } return _kadm5_error_code(ret); } diff --git a/lib/kadm5/rename_s.c b/lib/kadm5/rename_s.c index 2f6076b42..9b8019840 100644 --- a/lib/kadm5/rename_s.c +++ b/lib/kadm5/rename_s.c @@ -53,15 +53,18 @@ kadm5_s_rename_principal(void *server_handle, if(ret) return ret; } + + ret = kadm5_log_init(context); + if (ret) + goto out; + ret = context->db->hdb_fetch_kvno(context->context, context->db, source, HDB_F_GET_ANY|HDB_F_ADMIN_DATA, 0, &ent); - if(ret){ - if (!context->keep_open) - context->db->hdb_close(context->context, context->db); + if (ret) goto out; - } + oldname = ent.entry.principal; ret = _kadm5_set_modifier(context, &ent.entry); - if(ret) + if (ret) goto out2; { /* fix salt */ @@ -85,31 +88,30 @@ kadm5_s_rename_principal(void *server_handle, } krb5_free_salt(context->context, salt2); } - if(ret) + if (ret) goto out2; - oldname = ent.entry.principal; + + /* Borrow target */ ent.entry.principal = target; - ret = hdb_seal_keys(context->context, context->db, &ent.entry); - if (ret) { - ent.entry.principal = oldname; + if (ret) goto out2; - } - kadm5_log_rename (context, source, &ent.entry); + /* This logs the change for iprop and writes to the HDB */ + ret = kadm5_log_rename(context, source, &ent.entry); - ret = context->db->hdb_store(context->context, context->db, 0, &ent); - if(ret){ - ent.entry.principal = oldname; - goto out2; - } - ret = context->db->hdb_remove(context->context, context->db, oldname); - ent.entry.principal = oldname; out2: - if (!context->keep_open) - context->db->hdb_close(context->context, context->db); + ent.entry.principal = oldname; /* Unborrow target */ hdb_free_entry(context->context, &ent); + out: + (void) kadm5_log_end(context); + if (!context->keep_open) { + kadm5_ret_t ret2; + ret2 = context->db->hdb_close(context->context, context->db); + if (ret == 0 && ret2 != 0) + ret = ret2; + } return _kadm5_error_code(ret); } diff --git a/lib/kadm5/version-script.map b/lib/kadm5/version-script.map index fa8749bff..cc1282d50 100644 --- a/lib/kadm5/version-script.map +++ b/lib/kadm5/version-script.map @@ -62,16 +62,21 @@ HEIMDAL_KAMD5_SERVER_1.0 { kadm5_s_init_with_creds; kadm5_s_chpass_principal_cond; kadm5_log_set_version; + kadm5_log_signal_master; kadm5_log_signal_socket; kadm5_log_previous; kadm5_log_goto_end; kadm5_log_foreach; kadm5_log_get_version_fd; kadm5_log_get_version; + kadm5_log_recover; kadm5_log_replay; kadm5_log_end; kadm5_log_reinit; kadm5_log_init; + kadm5_log_init_nb; + kadm5_log_init_nolock; + kadm5_log_init_sharedlock; kadm5_log_nop; kadm5_log_truncate; kadm5_log_modify; diff --git a/lib/krb5/krb5.conf.5 b/lib/krb5/krb5.conf.5 index 9586dbc0a..894fa7db6 100644 --- a/lib/krb5/krb5.conf.5 +++ b/lib/krb5/krb5.conf.5 @@ -573,7 +573,25 @@ Use this file for the ACL list of this database. Use this file as the log of changes performed to the database. This file is used by .Nm ipropd-master -for propagating changes to slaves. +for propagating changes to slaves. It is also used by +.Nm kadmind +and +.Nm kadmin +(when used with the +.Li -l +option), and by all applications using +.Nm libkadm5 +with the local backend, for two-phase commit functionality. Slaves also +use this. Setting this to +.Nm /dev/null +disables two-phase commit and incremental propagation. Use +.Nm iprop-log +to show the contents of this log file. +.It Li log-max-size = Pa number +When the log reaches this size (in bytes), the log will be truncated, +saving some entries, and keeping the latest version number so as to not +disrupt incremental propagation. If set to a negative value then +automatic log truncation will be disabled. Defaults to 52428800 (50MB). .El .It Li } .It Li max-request = Va SIZE diff --git a/lib/krb5/libkrb5-exports.def.in b/lib/krb5/libkrb5-exports.def.in index 575788a48..60cf24c12 100644 --- a/lib/krb5/libkrb5-exports.def.in +++ b/lib/krb5/libkrb5-exports.def.in @@ -751,6 +751,7 @@ EXPORTS _krb5_dh_group_ok _krb5_get_host_realm_int _krb5_get_int + _krb5_get_int64 _krb5_pac_sign _krb5_parse_moduli _krb5_pk_kdf diff --git a/lib/krb5/version-script.map b/lib/krb5/version-script.map index 9255a435b..dc3cc63dc 100644 --- a/lib/krb5/version-script.map +++ b/lib/krb5/version-script.map @@ -744,6 +744,7 @@ HEIMDAL_KRB5_2.0 { _krb5_dh_group_ok; _krb5_get_host_realm_int; _krb5_get_int; + _krb5_get_int64; _krb5_pac_sign; _krb5_parse_moduli; _krb5_pk_kdf; diff --git a/tests/kdc/check-iprop.in b/tests/kdc/check-iprop.in index 9cf0310de..cfe4b637a 100644 --- a/tests/kdc/check-iprop.in +++ b/tests/kdc/check-iprop.in @@ -86,6 +86,35 @@ ${kadmin} -l ext -k ${keytab} iprop/slave.test.h5l.se@${R} || exit 1 echo foo > ${objdir}/foopassword +echo "Test log recovery" +${kadmin} -l add --random-key --use-defaults recovtest@${R} || exit 1 +# Test theory: save the log, make a change and save the record it +# produced, restore the log, append to it the saved record, then get + +# Save the log +cp current.log current.log.tmp +ls -l current.log.tmp | awk '{print $5}' > tmp +read sz < tmp +# Make a change +${kadmin} -l mod -a requires-pre-auth recovtest@${R} || exit 1 +${kadmin} -l get recovtest@${R} | grep 'Attributes: requires-pre-auth$' > /dev/null || exit 1 +# Save the resulting log record +ls -l current.log | awk '{print $5}' > tmp +read nsz < tmp +rm tmp +dd bs=1 if=current.log skip=$sz of=current.log.tmp.saved-record count=`expr $nsz - $sz` 2>/dev/null +# Undo the change +${kadmin} -l mod -a -requires-pre-auth recovtest@${R} || exit 1 +${kadmin} -l get recovtest@${R} | grep 'Attributes:.$' > /dev/null || exit 1 +# Restore the log +cp current.log current.log.save +mv current.log.tmp current.log +# Append the saved record +cat current.log.tmp.saved-record >> current.log +rm current.log.tmp.saved-record +# Check that we still see the principal as modified +${kadmin} -l get recovtest@${R} | grep 'Attributes: requires-pre-auth$' > /dev/null || exit 1 + # -- foo ipds= ipdm= @@ -214,6 +243,12 @@ ${EGREP} 'up-to-date with version' iprop-slave-status >/dev/null || { echo "slav echo "checking for replay problems" ${EGREP} 'Entry already exists in database' messages.log && exit 1 +echo "compare versions on master and slave logs (no lock)" +KRB5_CONFIG=${objdir}/krb5-slave.conf \ +${iprop_log} last-version -n > slave-last.tmp +${iprop_log} last-version -n > master-last.tmp +cmp master-last.tmp slave-last.tmp || exit 1 + echo "kill slave and remove log and database" sh ${leaks_kill} ipropd-slave $ipds || exit 1 sleep 2 @@ -241,7 +276,7 @@ ${kadmin} -l cpw --random-password user@${R} > /dev/null || exit 1 sleep 2 echo "live truncate on master log" -${iprop_log} truncate || exit 1 +${iprop_log} truncate -K 5 || exit 1 sleep 2 echo "Killing master and slave" @@ -351,4 +386,4 @@ if [ "$db_type" = lmdb ]; then [ "`expr 1 + "$princs"`" -eq "$entries" ] || exit 1 fi -exit $ec +exit 0 diff --git a/tests/kdc/krb5.conf.in b/tests/kdc/krb5.conf.in index 4359e53d9..6248a057e 100644 --- a/tests/kdc/krb5.conf.in +++ b/tests/kdc/krb5.conf.in @@ -97,6 +97,7 @@ signal_socket = @objdir@/signal iprop-stats = @objdir@/iprop-stats iprop-acl = @srcdir@/iprop-acl + log-max-size = 40000 [hdb] db-dir = @objdir@