Skip to content

Commit a548e14

Browse files
committed
fd-util: add new acquire_data_fd() API helper
All this function does is place some data in an in-memory read-only fd, that may be read back to get the original data back. Doing this in a way that works everywhere, given the different kernels we support as well as different privilege levels is surprisingly complex.
1 parent 9bd6a50 commit a548e14

File tree

3 files changed

+219
-0
lines changed

3 files changed

+219
-0
lines changed

src/basic/fd-util.c

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@
2626

2727
#include "dirent-util.h"
2828
#include "fd-util.h"
29+
#include "fileio.h"
2930
#include "fs-util.h"
3031
#include "macro.h"
32+
#include "memfd-util.h"
3133
#include "missing.h"
3234
#include "parse-util.h"
3335
#include "path-util.h"
@@ -421,3 +423,158 @@ int move_fd(int from, int to, int cloexec) {
421423

422424
return to;
423425
}
426+
427+
int acquire_data_fd(const void *data, size_t size, unsigned flags) {
428+
429+
char procfs_path[strlen("/proc/self/fd/") + DECIMAL_STR_MAX(int)];
430+
_cleanup_close_pair_ int pipefds[2] = { -1, -1 };
431+
char pattern[] = "/dev/shm/data-fd-XXXXXX";
432+
_cleanup_close_ int fd = -1;
433+
int isz = 0, r;
434+
ssize_t n;
435+
off_t f;
436+
437+
assert(data || size == 0);
438+
439+
/* Acquire a read-only file descriptor that when read from returns the specified data. This is much more
440+
* complex than I wish it was. But here's why:
441+
*
442+
* a) First we try to use memfds. They are the best option, as we can seal them nicely to make them
443+
* read-only. Unfortunately they require kernel 3.17, and – at the time of writing – we still support 3.14.
444+
*
445+
* b) Then, we try classic pipes. They are the second best options, as we can close the writing side, retaining
446+
* a nicely read-only fd in the reading side. However, they are by default quite small, and unprivileged
447+
* clients can only bump their size to a system-wide limit, which might be quite low.
448+
*
449+
* c) Then, we try an O_TMPFILE file in /dev/shm (that dir is the only suitable one known to exist from
450+
* earliest boot on). To make it read-only we open the fd a second time with O_RDONLY via
451+
* /proc/self/<fd>. Unfortunately O_TMPFILE is not available on older kernels on tmpfs.
452+
*
453+
* d) Finally, we try creating a regular file in /dev/shm, which we then delete.
454+
*
455+
* It sucks a bit that depending on the situation we return very different objects here, but that's Linux I
456+
* figure. */
457+
458+
if (size == 0 && ((flags & ACQUIRE_NO_DEV_NULL) == 0)) {
459+
/* As a special case, return /dev/null if we have been called for an empty data block */
460+
r = open("/dev/null", O_RDONLY|O_CLOEXEC|O_NOCTTY);
461+
if (r < 0)
462+
return -errno;
463+
464+
return r;
465+
}
466+
467+
if ((flags & ACQUIRE_NO_MEMFD) == 0) {
468+
fd = memfd_new("data-fd");
469+
if (fd < 0)
470+
goto try_pipe;
471+
472+
n = write(fd, data, size);
473+
if (n < 0)
474+
return -errno;
475+
if ((size_t) n != size)
476+
return -EIO;
477+
478+
f = lseek(fd, 0, SEEK_SET);
479+
if (f != 0)
480+
return -errno;
481+
482+
r = memfd_set_sealed(fd);
483+
if (r < 0)
484+
return r;
485+
486+
r = fd;
487+
fd = -1;
488+
489+
return r;
490+
}
491+
492+
try_pipe:
493+
if ((flags & ACQUIRE_NO_PIPE) == 0) {
494+
if (pipe2(pipefds, O_CLOEXEC|O_NONBLOCK) < 0)
495+
return -errno;
496+
497+
isz = fcntl(pipefds[1], F_GETPIPE_SZ, 0);
498+
if (isz < 0)
499+
return -errno;
500+
501+
if ((size_t) isz < size) {
502+
isz = (int) size;
503+
if (isz < 0 || (size_t) isz != size)
504+
return -E2BIG;
505+
506+
/* Try to bump the pipe size */
507+
(void) fcntl(pipefds[1], F_SETPIPE_SZ, isz);
508+
509+
/* See if that worked */
510+
isz = fcntl(pipefds[1], F_GETPIPE_SZ, 0);
511+
if (isz < 0)
512+
return -errno;
513+
514+
if ((size_t) isz < size)
515+
goto try_dev_shm;
516+
}
517+
518+
n = write(pipefds[1], data, size);
519+
if (n < 0)
520+
return -errno;
521+
if ((size_t) n != size)
522+
return -EIO;
523+
524+
(void) fd_nonblock(pipefds[0], false);
525+
526+
r = pipefds[0];
527+
pipefds[0] = -1;
528+
529+
return r;
530+
}
531+
532+
try_dev_shm:
533+
if ((flags & ACQUIRE_NO_TMPFILE) == 0) {
534+
fd = open("/dev/shm", O_RDWR|O_TMPFILE|O_CLOEXEC, 0500);
535+
if (fd < 0)
536+
goto try_dev_shm_without_o_tmpfile;
537+
538+
n = write(fd, data, size);
539+
if (n < 0)
540+
return -errno;
541+
if ((size_t) n != size)
542+
return -EIO;
543+
544+
/* Let's reopen the thing, in order to get an O_RDONLY fd for the original O_RDWR one */
545+
xsprintf(procfs_path, "/proc/self/fd/%i", fd);
546+
r = open(procfs_path, O_RDONLY|O_CLOEXEC);
547+
if (r < 0)
548+
return -errno;
549+
550+
return r;
551+
}
552+
553+
try_dev_shm_without_o_tmpfile:
554+
if ((flags & ACQUIRE_NO_REGULAR) == 0) {
555+
fd = mkostemp_safe(pattern);
556+
if (fd < 0)
557+
return fd;
558+
559+
n = write(fd, data, size);
560+
if (n < 0) {
561+
r = -errno;
562+
goto unlink_and_return;
563+
}
564+
if ((size_t) n != size) {
565+
r = -EIO;
566+
goto unlink_and_return;
567+
}
568+
569+
/* Let's reopen the thing, in order to get an O_RDONLY fd for the original O_RDWR one */
570+
r = open(pattern, O_RDONLY|O_CLOEXEC);
571+
if (r < 0)
572+
r = -errno;
573+
574+
unlink_and_return:
575+
(void) unlink(pattern);
576+
return r;
577+
}
578+
579+
return -EOPNOTSUPP;
580+
}

src/basic/fd-util.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,16 @@ int fd_get_path(int fd, char **ret);
7777

7878
int move_fd(int from, int to, int cloexec);
7979

80+
enum {
81+
ACQUIRE_NO_DEV_NULL = 1 << 0,
82+
ACQUIRE_NO_MEMFD = 1 << 1,
83+
ACQUIRE_NO_PIPE = 1 << 2,
84+
ACQUIRE_NO_TMPFILE = 1 << 3,
85+
ACQUIRE_NO_REGULAR = 1 << 4,
86+
};
87+
88+
int acquire_data_fd(const void *data, size_t size, unsigned flags);
89+
8090
/* Hint: ENETUNREACH happens if we try to connect to "non-existing" special IP addresses, such as ::5 */
8191
#define ERRNO_IS_DISCONNECT(r) \
8292
IN_SET(r, ENOTCONN, ECONNRESET, ECONNREFUSED, ECONNABORTED, EPIPE, ENETUNREACH)

src/test/test-fd-util.c

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
#include "fd-util.h"
2525
#include "fileio.h"
2626
#include "macro.h"
27+
#include "random-util.h"
28+
#include "string-util.h"
29+
#include "util.h"
2730

2831
static void test_close_many(void) {
2932
int fds[3];
@@ -103,11 +106,60 @@ static void test_open_serialization_fd(void) {
103106
write(fd, "test\n", 5);
104107
}
105108

109+
static void test_acquire_data_fd_one(unsigned flags) {
110+
char wbuffer[196*1024 - 7];
111+
char rbuffer[sizeof(wbuffer)];
112+
int fd;
113+
114+
fd = acquire_data_fd("foo", 3, flags);
115+
assert_se(fd >= 0);
116+
117+
zero(rbuffer);
118+
assert_se(read(fd, rbuffer, sizeof(rbuffer)) == 3);
119+
assert_se(streq(rbuffer, "foo"));
120+
121+
fd = safe_close(fd);
122+
123+
fd = acquire_data_fd("", 0, flags);
124+
assert_se(fd >= 0);
125+
126+
zero(rbuffer);
127+
assert_se(read(fd, rbuffer, sizeof(rbuffer)) == 0);
128+
assert_se(streq(rbuffer, ""));
129+
130+
fd = safe_close(fd);
131+
132+
random_bytes(wbuffer, sizeof(wbuffer));
133+
134+
fd = acquire_data_fd(wbuffer, sizeof(wbuffer), flags);
135+
assert_se(fd >= 0);
136+
137+
zero(rbuffer);
138+
assert_se(read(fd, rbuffer, sizeof(rbuffer)) == sizeof(rbuffer));
139+
assert_se(memcmp(rbuffer, wbuffer, sizeof(rbuffer)) == 0);
140+
141+
fd = safe_close(fd);
142+
}
143+
144+
static void test_acquire_data_fd(void) {
145+
146+
test_acquire_data_fd_one(0);
147+
test_acquire_data_fd_one(ACQUIRE_NO_DEV_NULL);
148+
test_acquire_data_fd_one(ACQUIRE_NO_MEMFD);
149+
test_acquire_data_fd_one(ACQUIRE_NO_DEV_NULL|ACQUIRE_NO_MEMFD);
150+
test_acquire_data_fd_one(ACQUIRE_NO_PIPE);
151+
test_acquire_data_fd_one(ACQUIRE_NO_DEV_NULL|ACQUIRE_NO_PIPE);
152+
test_acquire_data_fd_one(ACQUIRE_NO_MEMFD|ACQUIRE_NO_PIPE);
153+
test_acquire_data_fd_one(ACQUIRE_NO_DEV_NULL|ACQUIRE_NO_MEMFD|ACQUIRE_NO_PIPE);
154+
test_acquire_data_fd_one(ACQUIRE_NO_DEV_NULL|ACQUIRE_NO_MEMFD|ACQUIRE_NO_PIPE|ACQUIRE_NO_TMPFILE);
155+
}
156+
106157
int main(int argc, char *argv[]) {
107158
test_close_many();
108159
test_close_nointr();
109160
test_same_fd();
110161
test_open_serialization_fd();
162+
test_acquire_data_fd();
111163

112164
return 0;
113165
}

0 commit comments

Comments
 (0)