| // SPDX-License-Identifier: GPL-2.0 | |
| #define _GNU_SOURCE | |
| #include <errno.h> | |
| #include <fcntl.h> | |
| #include <limits.h> | |
| #include <sched.h> | |
| #include <stdarg.h> | |
| #include <stdbool.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <sys/mount.h> | |
| #include <sys/stat.h> | |
| #include <sys/types.h> | |
| #include <sys/vfs.h> | |
| #include <unistd.h> | |
| #ifndef MS_NOSYMFOLLOW | |
| # define MS_NOSYMFOLLOW 256 /* Do not follow symlinks */ | |
| #endif | |
| #ifndef ST_NOSYMFOLLOW | |
| # define ST_NOSYMFOLLOW 0x2000 /* Do not follow symlinks */ | |
| #endif | |
| #define DATA "/tmp/data" | |
| #define LINK "/tmp/symlink" | |
| #define TMP "/tmp" | |
| static void die(char *fmt, ...) | |
| { | |
| va_list ap; | |
| va_start(ap, fmt); | |
| vfprintf(stderr, fmt, ap); | |
| va_end(ap); | |
| exit(EXIT_FAILURE); | |
| } | |
| static void vmaybe_write_file(bool enoent_ok, char *filename, char *fmt, | |
| va_list ap) | |
| { | |
| ssize_t written; | |
| char buf[4096]; | |
| int buf_len; | |
| int fd; | |
| buf_len = vsnprintf(buf, sizeof(buf), fmt, ap); | |
| if (buf_len < 0) | |
| die("vsnprintf failed: %s\n", strerror(errno)); | |
| if (buf_len >= sizeof(buf)) | |
| die("vsnprintf output truncated\n"); | |
| fd = open(filename, O_WRONLY); | |
| if (fd < 0) { | |
| if ((errno == ENOENT) && enoent_ok) | |
| return; | |
| die("open of %s failed: %s\n", filename, strerror(errno)); | |
| } | |
| written = write(fd, buf, buf_len); | |
| if (written != buf_len) { | |
| if (written >= 0) { | |
| die("short write to %s\n", filename); | |
| } else { | |
| die("write to %s failed: %s\n", | |
| filename, strerror(errno)); | |
| } | |
| } | |
| if (close(fd) != 0) | |
| die("close of %s failed: %s\n", filename, strerror(errno)); | |
| } | |
| static void maybe_write_file(char *filename, char *fmt, ...) | |
| { | |
| va_list ap; | |
| va_start(ap, fmt); | |
| vmaybe_write_file(true, filename, fmt, ap); | |
| va_end(ap); | |
| } | |
| static void write_file(char *filename, char *fmt, ...) | |
| { | |
| va_list ap; | |
| va_start(ap, fmt); | |
| vmaybe_write_file(false, filename, fmt, ap); | |
| va_end(ap); | |
| } | |
| static void create_and_enter_ns(void) | |
| { | |
| uid_t uid = getuid(); | |
| gid_t gid = getgid(); | |
| if (unshare(CLONE_NEWUSER) != 0) | |
| die("unshare(CLONE_NEWUSER) failed: %s\n", strerror(errno)); | |
| maybe_write_file("/proc/self/setgroups", "deny"); | |
| write_file("/proc/self/uid_map", "0 %d 1", uid); | |
| write_file("/proc/self/gid_map", "0 %d 1", gid); | |
| if (setgid(0) != 0) | |
| die("setgid(0) failed %s\n", strerror(errno)); | |
| if (setuid(0) != 0) | |
| die("setuid(0) failed %s\n", strerror(errno)); | |
| if (unshare(CLONE_NEWNS) != 0) | |
| die("unshare(CLONE_NEWNS) failed: %s\n", strerror(errno)); | |
| } | |
| static void setup_symlink(void) | |
| { | |
| int data, err; | |
| data = creat(DATA, O_RDWR); | |
| if (data < 0) | |
| die("creat failed: %s\n", strerror(errno)); | |
| err = symlink(DATA, LINK); | |
| if (err < 0) | |
| die("symlink failed: %s\n", strerror(errno)); | |
| if (close(data) != 0) | |
| die("close of %s failed: %s\n", DATA, strerror(errno)); | |
| } | |
| static void test_link_traversal(bool nosymfollow) | |
| { | |
| int link; | |
| link = open(LINK, 0, O_RDWR); | |
| if (nosymfollow) { | |
| if ((link != -1 || errno != ELOOP)) { | |
| die("link traversal unexpected result: %d, %s\n", | |
| link, strerror(errno)); | |
| } | |
| } else { | |
| if (link < 0) | |
| die("link traversal failed: %s\n", strerror(errno)); | |
| if (close(link) != 0) | |
| die("close of link failed: %s\n", strerror(errno)); | |
| } | |
| } | |
| static void test_readlink(void) | |
| { | |
| char buf[4096]; | |
| ssize_t ret; | |
| bzero(buf, sizeof(buf)); | |
| ret = readlink(LINK, buf, sizeof(buf)); | |
| if (ret < 0) | |
| die("readlink failed: %s\n", strerror(errno)); | |
| if (strcmp(buf, DATA) != 0) | |
| die("readlink strcmp failed: '%s' '%s'\n", buf, DATA); | |
| } | |
| static void test_realpath(void) | |
| { | |
| char *path = realpath(LINK, NULL); | |
| if (!path) | |
| die("realpath failed: %s\n", strerror(errno)); | |
| if (strcmp(path, DATA) != 0) | |
| die("realpath strcmp failed\n"); | |
| free(path); | |
| } | |
| static void test_statfs(bool nosymfollow) | |
| { | |
| struct statfs buf; | |
| int ret; | |
| ret = statfs(TMP, &buf); | |
| if (ret) | |
| die("statfs failed: %s\n", strerror(errno)); | |
| if (nosymfollow) { | |
| if ((buf.f_flags & ST_NOSYMFOLLOW) == 0) | |
| die("ST_NOSYMFOLLOW not set on %s\n", TMP); | |
| } else { | |
| if ((buf.f_flags & ST_NOSYMFOLLOW) != 0) | |
| die("ST_NOSYMFOLLOW set on %s\n", TMP); | |
| } | |
| } | |
| static void run_tests(bool nosymfollow) | |
| { | |
| test_link_traversal(nosymfollow); | |
| test_readlink(); | |
| test_realpath(); | |
| test_statfs(nosymfollow); | |
| } | |
| int main(int argc, char **argv) | |
| { | |
| create_and_enter_ns(); | |
| if (mount("testing", TMP, "ramfs", 0, NULL) != 0) | |
| die("mount failed: %s\n", strerror(errno)); | |
| setup_symlink(); | |
| run_tests(false); | |
| if (mount("testing", TMP, "ramfs", MS_REMOUNT|MS_NOSYMFOLLOW, NULL) != 0) | |
| die("remount failed: %s\n", strerror(errno)); | |
| run_tests(true); | |
| return EXIT_SUCCESS; | |
| } |