diff --git a/bend2/src/host/process-spawn.c b/bend2/src/host/process-spawn.c index c86881d5e..9e9d154a9 100644 --- a/bend2/src/host/process-spawn.c +++ b/bend2/src/host/process-spawn.c @@ -3,6 +3,7 @@ #include #include #include +#include extern char **environ; @@ -1213,6 +1214,131 @@ static int br_observer_pid(int socket,pid_t *pid) { *pid=peer.pid;return 0; #endif } +/* A live replacement must identify the supplied process from the exact retained + attempt it is observing. The pidfd remains open through handoff and retirement. */ +static int br_observer_attempt_custody(pid_t pid,const char *database,const char *session, + const char *directory,int lock,int *life_out) { + if(!life_out)return EINVAL; + *life_out=-1; + char *canonical=realpath(directory,NULL),*database_path=realpath(database,NULL); + if(!canonical || !database_path) {free(canonical);free(database_path);return errno;} + char *stdout_path=br_path(canonical,"stdout"); + char *log_path=br_path(canonical,"observer.log"); + int error=!stdout_path || !log_path?ENOMEM:0; + int ended=0,life=-1; + if(!error) { + life=br_process_lifetime(pid,&ended); + if(life<0)error=ended?ESRCH:errno; + } +#ifndef __APPLE__ + char command_path[PATH_MAX]; + unsigned char command[65536]; + size_t command_length=0; + if(!error) { + int n=snprintf(command_path,sizeof(command_path),"/proc/%ld/cmdline",(long)pid); + if(n<0 || (size_t)n>=sizeof(command_path))error=EOVERFLOW; + } + if(!error) { + int fd=open(command_path,O_RDONLY|O_CLOEXEC); + if(fd<0)error=errno; + else { + while(command_length>4] || encoded[10+i*2]!=hex[c&15]){error=EPERM;break;} + } + } + if(!error && recovery) { + char *end=NULL;errno=0;unsigned long prior=strtoul(argv[4],&end,10); + if(errno || end==argv[4] || *end || !prior || prior>INT_MAX)error=EPERM; + } + } + } + + if(!error) { + struct pollfd alive={life,POLLIN,0}; + int ready;do {ready=poll(&alive,1,0);}while(ready<0 && errno==EINTR); + if(ready<0)error=errno; + else if(ready || (alive.revents&(POLLIN|POLLHUP|POLLERR)))error=ESRCH; + } + if(!error) { + struct stat lock_info; + if(fstat(lock,&lock_info))error=errno; + char fd_directory[PATH_MAX],fd_path[PATH_MAX],target[PATH_MAX+1]; + int length=snprintf(fd_directory,sizeof(fd_directory),"/proc/%ld/fd",(long)pid); + if(!error && (length<0 || (size_t)length>=sizeof(fd_directory)))error=EOVERFLOW; + DIR *fds=!error?opendir(fd_directory):NULL; + if(!error && !fds)error=errno; + int stdout_owned=0,log_owned=0,lock_owned=0; + struct dirent *entry; + while(!error && (entry=readdir(fds))) { + if(entry->d_name[0]=='.')continue; + length=snprintf(fd_path,sizeof(fd_path),"%s/%s",fd_directory,entry->d_name); + if(length<0 || (size_t)length>=sizeof(fd_path)){error=EOVERFLOW;break;} + ssize_t n=readlink(fd_path,target,sizeof(target)-1); + if(n>=0) { + target[n]=0; + if(!strcmp(target,stdout_path))stdout_owned=1; + if(!strcmp(target,log_path))log_owned=1; + } + struct stat info; + if(!stat(fd_path,&info) && info.st_dev==lock_info.st_dev && info.st_ino==lock_info.st_ino)lock_owned=1; + if(stdout_owned && log_owned && lock_owned)break; + } + if(fds)closedir(fds); + if(!error && (!stdout_owned || !log_owned || !lock_owned))error=EPERM; + } + if(!error) { + struct pollfd alive={life,POLLIN,0}; + int ready;do {ready=poll(&alive,1,0);}while(ready<0 && errno==EINTR); + if(ready<0)error=errno; + else if(ready || (alive.revents&(POLLIN|POLLHUP|POLLERR)))error=ESRCH; + } +#else + if(!error)error=ENOTSUP; +#endif + free(stdout_path);free(log_path);free(canonical);free(database_path); + if(error) {if(life>=0)close(life);return error;} + *life_out=life; + return 0; +} + static int br_control_command(BrKeeper *keeper,BrControl *control,BrFrame frame,const char *payload) { const char *end=memchr(payload,0,(size_t)frame.length); int error=0; @@ -4969,7 +5095,7 @@ static void __attribute__((constructor)) baton_process_signals(void){signal(SIGP typedef struct { char *database, *session, *path, *directory; const char *detail; - int handle, error, kind, keeper; + int handle, error, kind, keeper, observer_life; u32 observer; BatonProcessCall attempt; } BatonSessionLock; @@ -4979,19 +5105,38 @@ static int baton_observer_completed(BatonSessionLock *call) { int error=br_admission_verify(call->directory,call->database,-1); if(error)return error; char *path=br_path(call->directory,"status"); - FILE *file=path?fopen(path,"r"):NULL; - error=file?0:path?errno:ENOMEM; + if(!path)return ENOMEM; + errno=0; + FILE *file=fopen(path,"r"); + int status_error=file?0:errno; free(path); - int status=0; + int status=0,live_pending=0; if(file) { - if(fscanf(file,"%d",&status)!=1)error=EINVAL; - else if(!WIFEXITED(status) && !WIFSIGNALED(status))error=EBUSY; + if(fscanf(file,"%d",&status)!=1)status_error=EINVAL; + else if(!WIFEXITED(status) && !WIFSIGNALED(status))status_error=EBUSY; fclose(file); + } else if(status_error==ENOENT) { + live_pending=1; + status_error=0; } - if(error) {call->detail="Observer recovery requires the retained native exit status.";return error;} + if(status_error) {call->detail="Observer recovery requires a valid retained native status or a live native birth.";return status_error;} BrBirth native; if((error=br_read_file(call->directory,"native.birth",&native,sizeof(native))))return error; if(call->observer==(u32)native.pid)return EINVAL; + if(live_pending) { + int ended=0,life=br_lifetime(call->directory,&ended); + if(life<0) { + error=ended?ESRCH:errno; + call->detail="Live observer recovery requires the recorded native birth to remain alive."; + return error; + } + close(life); + error=br_observer_attempt_custody((pid_t)call->observer,call->database,call->session,call->directory,call->handle,&call->observer_life); + if(error) { + call->detail="Live observer recovery requires the supplied process to hold this attempt's stdout and observer log."; + return error; + } + } BrManifest manifest={0}; if((error=br_manifest_read(call->directory,&manifest))) { br_manifest_free(&manifest);return error; @@ -5006,25 +5151,36 @@ static int baton_observer_completed(BatonSessionLock *call) { call->keeper=1; } else { error=errno; - if(error==ENOENT || error==ECONNREFUSED)error=0; + if(!live_pending && (error==ENOENT || error==ECONNREFUSED))error=0; } } if(probe>=0)close(probe); + if(!error && live_pending && !call->keeper) { + error=ECONNREFUSED; + call->detail="Live observer recovery requires the active native keeper."; + } return error; } static int baton_observer_retire(BatonSessionLock *call) { - int ended=0,life=br_process_lifetime((pid_t)call->observer,&ended); - if(life<0 && !ended)return errno; + int ended=0,life=call->observer_life; + call->observer_life=-1; + if(life<0)life=br_process_lifetime((pid_t)call->observer,&ended); + if(life<0)return ended?0:errno; int error=0; +#ifdef SYS_pidfd_send_signal + if(syscall(SYS_pidfd_send_signal,life,SIGTERM,NULL,0) && errno!=ESRCH)error=errno; + if(!error && syscall(SYS_pidfd_send_signal,life,SIGCONT,NULL,0) && errno!=ESRCH)error=errno; +#else if(kill((pid_t)call->observer,SIGTERM) && errno!=ESRCH)error=errno; if(!error && kill((pid_t)call->observer,SIGCONT) && errno!=ESRCH)error=errno; - if(!error && life>=0) { +#endif + if(!error) { struct pollfd observed={life,POLLIN,0}; int ready;do {ready=poll(&observed,1,-1);}while(ready<0 && errno==EINTR); if(ready<0)error=errno; } - if(life>=0)close(life); + close(life); return error; } @@ -5146,6 +5302,7 @@ static Term baton_session_lock_pack(Env e, IoWork *w) { } } Term result=call->error ? io_fail(e,call->error,call->detail) : io_done(e,value); + if(call->observer_life>=0)close(call->observer_life); free(call->database);free(call->session);free(call->path);free(call->directory);free(call); w->data=NULL; return result; @@ -5155,6 +5312,7 @@ static Term baton_session_lock_begin(Env e, Term *f, IoWork *w, int kind) { BatonSessionLock *call=calloc(1,sizeof(*call)); if(!call) return io_fail(e,ENOMEM,NULL); call->kind=kind; + call->observer_life=-1; if(kind==3 || kind==5 || kind==6) { u64 dn=0,sn=0; call->database=io_cstr(e,f[0],&dn); diff --git a/bend2/src/host/sqlite.c b/bend2/src/host/sqlite.c index 8a832e36d..d02d80e34 100644 --- a/bend2/src/host/sqlite.c +++ b/bend2/src/host/sqlite.c @@ -60,6 +60,61 @@ static const char *const baton_change_schema[] = { NULL }; +/* Return whether a DROP TRIGGER is immediately followed by the same trigger + definition already stored in sqlite_schema. SQLite omits the final semicolon + from sqlite_schema.sql, so compare after trimming that terminator. */ +static int baton_sql_trigger_matches(sqlite3 *db, const char *drop_sql, + const char *create_sql, int *matches, + char **error) { + static const char drop_prefix[] = "DROP TRIGGER IF EXISTS "; + static const char create_prefix[] = "CREATE TRIGGER "; + *matches = 0; + if (strncmp(drop_sql, drop_prefix, sizeof(drop_prefix) - 1) != 0 || + strncmp(create_sql, create_prefix, sizeof(create_prefix) - 1) != 0) + return SQLITE_OK; + const char *drop_name = drop_sql + sizeof(drop_prefix) - 1; + size_t drop_name_length = strcspn(drop_name, "; \t\r\n"); + const char *create_name = create_sql + sizeof(create_prefix) - 1; + size_t create_name_length = strcspn(create_name, " \t\r\n"); + if (!drop_name_length || drop_name[drop_name_length] != ';' || + drop_name_length != create_name_length || + memcmp(drop_name, create_name, drop_name_length) != 0) + return SQLITE_OK; + + sqlite3_stmt *statement = NULL; + int code = sqlite3_prepare_v2(db, + "SELECT sql FROM sqlite_master WHERE type='trigger' AND name=?1;", + -1, &statement, NULL); + if (code == SQLITE_OK) + code = sqlite3_bind_text(statement, 1, drop_name, (int)drop_name_length, SQLITE_STATIC); + if (code == SQLITE_OK) { + code = sqlite3_step(statement); + if (code == SQLITE_ROW) { + const char *stored = (const char *)sqlite3_column_text(statement, 0); + size_t stored_length = (size_t)sqlite3_column_bytes(statement, 0); + size_t wanted_length = strlen(create_sql); + while (stored_length && (stored[stored_length - 1] == ';' || + stored[stored_length - 1] == ' ' || stored[stored_length - 1] == '\t' || + stored[stored_length - 1] == '\r' || stored[stored_length - 1] == '\n')) + stored_length--; + while (wanted_length && (create_sql[wanted_length - 1] == ';' || + create_sql[wanted_length - 1] == ' ' || create_sql[wanted_length - 1] == '\t' || + create_sql[wanted_length - 1] == '\r' || create_sql[wanted_length - 1] == '\n')) + wanted_length--; + *matches = stored && stored_length == wanted_length && + memcmp(stored, create_sql, wanted_length) == 0; + code = SQLITE_OK; + } else if (code == SQLITE_DONE) { + code = SQLITE_OK; + } + } + int final = sqlite3_finalize(statement); + if (code == SQLITE_OK && final != SQLITE_OK) code = final; + if (code != SQLITE_OK && error && !*error) + *error = sqlite3_mprintf("%s", sqlite3_errmsg(db)); + return code; +} + static int baton_sql_install_projection(sqlite3 *db, const char *schema, char **error) { int code = sqlite3_exec(db, "BEGIN IMMEDIATE", NULL, NULL, error); if (code == SQLITE_OK) @@ -74,8 +129,19 @@ static int baton_sql_install_projection(sqlite3 *db, const char *schema, char ** code = sqlite3_exec(db, "ALTER TABLE knowledge_promotions ADD COLUMN source_kind TEXT NOT NULL DEFAULT 'session';", NULL, NULL, error); if (code == SQLITE_OK && sqlite3_table_column_metadata(db, NULL, "knowledge_promotions", "destination_kind", NULL, NULL, NULL, NULL, NULL) != SQLITE_OK) code = sqlite3_exec(db, "ALTER TABLE knowledge_promotions ADD COLUMN destination_kind TEXT NOT NULL DEFAULT 'session';", NULL, NULL, error); - for (size_t i = 0; code == SQLITE_OK && baton_change_schema[i]; i++) - code = sqlite3_exec(db, baton_change_schema[i], NULL, NULL, error); + for (size_t i = 0; code == SQLITE_OK && baton_change_schema[i]; i++) { + int matches = 0; + if (baton_change_schema[i + 1] && + strncmp(baton_change_schema[i], "DROP TRIGGER IF EXISTS ", 23) == 0) + code = baton_sql_trigger_matches(db, baton_change_schema[i], + baton_change_schema[i + 1], &matches, error); + if (code == SQLITE_OK && matches) { + i++; + continue; + } + if (code == SQLITE_OK) + code = sqlite3_exec(db, baton_change_schema[i], NULL, NULL, error); + } if (code == SQLITE_OK) code = sqlite3_exec(db, "COMMIT", NULL, NULL, error); if (code != SQLITE_OK)