fix(credentials): bound-wait on FIFO reads so slow process substitution works
secret_file_open() opened secret files with O_NONBLOCK and only cleared it for S_ISREG, so on a FIFO/process-substitution source (--password-file <(...), --early-input <(...)) fgets() failed immediately with EAGAIN when the writer had not yet produced data, breaking slow producers. Keep O_NONBLOCK at open() (a writer-less FIFO must not block the open) and route all three readers through a new secret_read_line() helper. It accumulates a line across reads and, on EAGAIN/EWOULDBLOCK (or a partial line) with no newline and no EOF, clearerr()s and polls for readability against one overall CLOCK_MONOTONIC deadline of CREDENTIAL_FIFO_READ_TIMEOUT_MS (3000 ms); on timeout or a real read error it fails with a clear message. EOF finishes normally. Regular files are left blocking and read exactly as before. Handles a line split across several write()s and keeps the owner/mode fstat gate, O_NOFOLLOW and the /dev/fd/N exception unchanged.
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* Known-answer vector, independently recomputed with Python
|
||||
@@ -764,6 +765,160 @@ static void test_credentials_read_secret_file_fifo_no_hang() {
|
||||
unlink(fifo);
|
||||
}
|
||||
|
||||
/* Write `s` fully to `fd`, retrying EINTR. */
|
||||
static void write_all_fd(int fd, const char* s) {
|
||||
size_t total = strlen(s);
|
||||
size_t off = 0;
|
||||
while (off < total) {
|
||||
ssize_t w = write(fd, s + off, total - off);
|
||||
if (w < 0) {
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
off += (size_t)w;
|
||||
}
|
||||
}
|
||||
|
||||
/* Deterministically model a slow process substitution (`--password-file
|
||||
* <(sleep N; ...)`): attach a writer to the FIFO (so the reader sees EAGAIN --
|
||||
* the empty/no-writer FIFO instead yields an immediate EOF), have it sleep
|
||||
* `delay_ms`, then write `first` and, after another `delay_ms`, `second` (NULL
|
||||
* for a single write). Splitting across the delay exercises reassembly of a
|
||||
* line delivered by several write()s.
|
||||
*
|
||||
* The parent keeps a spare read end open for the lifetime of the test so the
|
||||
* writer always has a reader; the caller must close(*hold_out), waitpid() the
|
||||
* returned pid and unlink the FIFO. Returns the child pid, or -1 on setup
|
||||
* failure. */
|
||||
static pid_t fifo_writer_sleep_then_write(const char* fifo, const char* first, unsigned delay_ms,
|
||||
const char* second, int* hold_out) {
|
||||
int sync[2];
|
||||
if (pipe(sync) != 0)
|
||||
return -1;
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
close(sync[0]);
|
||||
close(sync[1]);
|
||||
return -1;
|
||||
}
|
||||
if (pid == 0) {
|
||||
close(sync[0]);
|
||||
int wfd = open(fifo, O_WRONLY | O_CLOEXEC);
|
||||
char ready = wfd >= 0 ? 1 : 0;
|
||||
if (write(sync[1], &ready, 1) != 1)
|
||||
_exit(1);
|
||||
close(sync[1]);
|
||||
if (wfd >= 0) {
|
||||
usleep(delay_ms * 1000);
|
||||
write_all_fd(wfd, first);
|
||||
if (second) {
|
||||
usleep(delay_ms * 1000);
|
||||
write_all_fd(wfd, second);
|
||||
}
|
||||
close(wfd);
|
||||
}
|
||||
_exit(0);
|
||||
}
|
||||
close(sync[1]);
|
||||
int hold = open(fifo, O_RDONLY | O_NONBLOCK | O_CLOEXEC);
|
||||
char ready = 0;
|
||||
ssize_t got = read(sync[0], &ready, 1);
|
||||
close(sync[0]);
|
||||
if (got != 1 || ready != 1) {
|
||||
if (hold >= 0)
|
||||
close(hold);
|
||||
return -1;
|
||||
}
|
||||
*hold_out = hold;
|
||||
return pid;
|
||||
}
|
||||
|
||||
/* A FIFO writer that produces its data after a short delay must be read
|
||||
* successfully (the regression: O_NONBLOCK made fgets fail with EAGAIN before
|
||||
* the writer ran). */
|
||||
static void test_credentials_read_secret_file_fifo_delayed_writer() {
|
||||
char err[512];
|
||||
char fifo[256];
|
||||
snprintf(fifo, sizeof(fifo), "/tmp/fs_cred_pwfifo_slow_%d_%d", (int)getpid(), g_file_counter++);
|
||||
unlink(fifo);
|
||||
EXPECT_EQ_INT(mkfifo(fifo, 0600), 0);
|
||||
|
||||
int hold = -1;
|
||||
pid_t writer =
|
||||
fifo_writer_sleep_then_write(fifo, "alice:correct horse battery staple\n", 250, NULL, &hold);
|
||||
EXPECT_TRUE(writer > 0);
|
||||
|
||||
char* user = NULL;
|
||||
char* password = NULL;
|
||||
EXPECT_EQ_INT(credentials_read_secret_file(fifo, &user, &password, err, sizeof(err)), 0);
|
||||
EXPECT_EQ_STR(user, "alice");
|
||||
EXPECT_EQ_STR(password, "correct horse battery staple");
|
||||
free(user);
|
||||
free(password);
|
||||
|
||||
int status = 0;
|
||||
waitpid(writer, &status, 0);
|
||||
if (hold >= 0)
|
||||
close(hold);
|
||||
unlink(fifo);
|
||||
}
|
||||
|
||||
/* The same, but the line is written in two chunks separated by the delay: the
|
||||
* reader must reassemble one line rather than parse the first chunk as an
|
||||
* empty-password entry. */
|
||||
static void test_credentials_read_secret_file_fifo_split_write() {
|
||||
char err[512];
|
||||
char fifo[256];
|
||||
snprintf(fifo, sizeof(fifo), "/tmp/fs_cred_pwfifo_split_%d_%d", (int)getpid(), g_file_counter++);
|
||||
unlink(fifo);
|
||||
EXPECT_EQ_INT(mkfifo(fifo, 0600), 0);
|
||||
|
||||
int hold = -1;
|
||||
pid_t writer =
|
||||
fifo_writer_sleep_then_write(fifo, "alice:correct horse", 200, " battery staple\n", &hold);
|
||||
EXPECT_TRUE(writer > 0);
|
||||
|
||||
char* user = NULL;
|
||||
char* password = NULL;
|
||||
EXPECT_EQ_INT(credentials_read_secret_file(fifo, &user, &password, err, sizeof(err)), 0);
|
||||
EXPECT_EQ_STR(user, "alice");
|
||||
EXPECT_EQ_STR(password, "correct horse battery staple");
|
||||
free(user);
|
||||
free(password);
|
||||
|
||||
int status = 0;
|
||||
waitpid(writer, &status, 0);
|
||||
if (hold >= 0)
|
||||
close(hold);
|
||||
unlink(fifo);
|
||||
}
|
||||
|
||||
/* The server-side store loader (--password-file / --early-input) must also
|
||||
* accept a FIFO whose writer appears after a delay. */
|
||||
static void test_credentials_store_fifo_delayed_writer() {
|
||||
char err[512];
|
||||
char fifo[256];
|
||||
snprintf(fifo, sizeof(fifo), "/tmp/fs_cred_storefifo_%d_%d", (int)getpid(), g_file_counter++);
|
||||
unlink(fifo);
|
||||
EXPECT_EQ_INT(mkfifo(fifo, 0600), 0);
|
||||
|
||||
int hold = -1;
|
||||
pid_t writer = fifo_writer_sleep_then_write(fifo, KAT_STORE_LINE "\n", 250, NULL, &hold);
|
||||
EXPECT_TRUE(writer > 0);
|
||||
|
||||
CredentialStore* store = credentials_load(fifo, NULL, err, sizeof(err));
|
||||
EXPECT_NOT_NULL(store);
|
||||
EXPECT_EQ_INT(credentials_store_size(store), 1);
|
||||
credentials_free(store);
|
||||
|
||||
int status = 0;
|
||||
waitpid(writer, &status, 0);
|
||||
if (hold >= 0)
|
||||
close(hold);
|
||||
rm_temp(fifo);
|
||||
}
|
||||
|
||||
/* fd-backed store paths (bash process substitution `<(...)`, i.e. /dev/fd/N and
|
||||
* /proc/self/fd/N) are symlinks, so the ordinary O_NOFOLLOW rule would reject
|
||||
* them with ELOOP. They name the calling process's own descriptors, so they
|
||||
@@ -1184,6 +1339,9 @@ void test_credentials(void) {
|
||||
test_credentials_read_secret_file_bad();
|
||||
test_credentials_read_secret_file_symlink_rejected();
|
||||
test_credentials_read_secret_file_fifo_no_hang();
|
||||
test_credentials_read_secret_file_fifo_delayed_writer();
|
||||
test_credentials_read_secret_file_fifo_split_write();
|
||||
test_credentials_store_fifo_delayed_writer();
|
||||
test_credentials_read_secret_file_fd_backed_accepted();
|
||||
test_credentials_hash_file();
|
||||
test_credentials_rejects_group_or_other_accessible();
|
||||
|
||||
Reference in New Issue
Block a user