Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 89 additions & 6 deletions kernel/actiondfs/actiondfs.c
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,7 @@ static int actiondfs_stage_mkdir_child(struct path *parent_path,
return err;
}

/* Returns a borrowed path, held by sbi and the directory node. */
static int actiondfs_ensure_stage_parent_path(struct actiondfs_sb_info *sbi,
struct actiondfs_node *node,
struct path *path)
Expand Down Expand Up @@ -1167,7 +1168,7 @@ static struct file *actiondfs_open_backing_cas_blob(struct actiondfs_sb_info *sb
}

open_start = actiondfs_stat_time_start();
file = backing_file_open(user_path, O_RDONLY, &real_path,
file = backing_file_open(user_path, O_RDONLY | O_LARGEFILE, &real_path,
current_cred());
actiondfs_stat_add_elapsed(ACTIONDFS_STAT_BLOB_OPEN_BACKING_FILE_NS,
open_start);
Expand Down Expand Up @@ -1226,7 +1227,8 @@ static struct file *actiondfs_open_staged_backing(struct actiondfs_sb_info *sbi,
}

phase_start = actiondfs_stat_time_start();
file = backing_file_open(file_user_path(actiondfs_file), flags,
/* Kernel backing opens bypass the syscall's force_o_largefile(). */
file = backing_file_open(file_user_path(actiondfs_file), flags | O_LARGEFILE,
&real_path, current_cred());
actiondfs_stat_add_elapsed(ACTIONDFS_STAT_STAGE_BACKING_OPEN_FILE_NS,
phase_start);
Expand Down Expand Up @@ -2301,12 +2303,17 @@ static void actiondfs_init_inode(struct inode *inode,
inode->i_fop = &actiondfs_dir_fops;
inode->i_opflags |= IOP_LOOKUP;
set_nlink(inode, 2);
} else if (S_ISFIFO(node->mode) || S_ISSOCK(node->mode)) {
init_special_inode(inode, node->mode, 0);
inode->i_op = &actiondfs_file_iops;
} else {
inode->i_op = &actiondfs_file_iops;
inode->i_fop = &actiondfs_file_fops;
inode->i_opflags |= IOP_NOFOLLOW;
i_size_write(inode, node->size);
}
if (node->origin == ACTIONDFS_NODE_STAGED && node->stage_dentry)
set_nlink(inode, READ_ONCE(d_inode(node->stage_dentry)->i_nlink));
inode->i_opflags |= IOP_FASTPERM;
}

Expand Down Expand Up @@ -2456,6 +2463,8 @@ static struct inode *actiondfs_lookup_staged_inode(struct inode *dir,
if (err)
goto out_error;
mode = S_IFLNK | 0777;
} else if (S_ISFIFO(mode) || S_ISSOCK(mode)) {
/* Action-local IPC nodes live only in the writable stage. */
} else {
goto out_negative;
}
Expand Down Expand Up @@ -2637,6 +2646,10 @@ static int actiondfs_create_staged_child(struct inode *dir,
d_inode(parent_path.dentry), real_dentry,
target);
inode_unlock(d_inode(parent_path.dentry));
} else if (S_ISFIFO(mode) || S_ISSOCK(mode)) {
err = vfs_mknod(mnt_idmap(parent_path.mnt),
d_inode(parent_path.dentry), real_dentry, mode, 0);
inode_unlock(d_inode(parent_path.dentry));
} else {
err = vfs_create(mnt_idmap(parent_path.mnt),
d_inode(parent_path.dentry), real_dentry,
Expand Down Expand Up @@ -2676,6 +2689,17 @@ static int actiondfs_create(struct mnt_idmap *idmap, struct inode *dir,
return err;
}

static int actiondfs_mknod(struct mnt_idmap *idmap, struct inode *dir,
struct dentry *dentry, umode_t mode, dev_t dev)
{
if (!actiondfs_sbi(dir->i_sb)->stage_path.dentry)
return -EROFS;
/* Device nodes must never expose guest devices to an action. */
if (!S_ISFIFO(mode) && !S_ISSOCK(mode))
return -EPERM;
return actiondfs_create_staged_child(dir, dentry, mode, NULL, false);
}

static int actiondfs_symlink(struct mnt_idmap *idmap, struct inode *dir,
struct dentry *dentry, const char *target)
{
Expand Down Expand Up @@ -2734,9 +2758,12 @@ static int actiondfs_remove_staged_child(struct inode *dir,
}
actiondfs_stage_unlock_child(&parent_path, real_dentry);
if (!err) {
clear_nlink(inode);
if (directory)
if (directory) {
clear_nlink(inode);
drop_nlink(dir);
} else {
drop_nlink(inode);
}
actiondfs_note_stage_change(parent);
}
out_drop_write:
Expand All @@ -2746,6 +2773,56 @@ static int actiondfs_remove_staged_child(struct inode *dir,
return err;
}

static int actiondfs_link(struct dentry *old_dentry, struct inode *dir,
struct dentry *dentry)
{
struct inode *inode = d_inode(old_dentry);
struct actiondfs_node *node = inode->i_private;
struct actiondfs_node *parent = dir->i_private;
struct actiondfs_sb_info *sbi = actiondfs_sbi(dir->i_sb);
struct path parent_path;
struct dentry *real_old;
struct dentry *real_new;
int err;

/* Never expose a writable alias of an immutable CAS input. */
if (node->origin != ACTIONDFS_NODE_STAGED)
return -EROFS;
/* Use the held inode's dentry, even after another alias was unlinked. */
real_old = READ_ONCE(node->stage_dentry);
if (!real_old)
return -ENOENT;
dget(real_old);
err = actiondfs_ensure_stage_parent_path(sbi, parent, &parent_path);
if (err)
goto out_put_old;
err = mnt_want_write(parent_path.mnt);
if (err)
goto out_put_old;
err = actiondfs_stage_lookup_child(&parent_path, dentry->d_name.name,
dentry->d_name.len, &real_new);
if (err)
goto out_drop_write;
if (d_inode(real_new))
err = -EEXIST;
else
err = vfs_link(real_old, mnt_idmap(parent_path.mnt),
d_inode(parent_path.dentry), real_new, NULL);
actiondfs_stage_unlock_child(&parent_path, real_new);
if (!err) {
inc_nlink(inode);
inode_set_ctime_current(inode);
ihold(inode);
d_instantiate(dentry, inode);
actiondfs_note_stage_change(parent);
}
out_drop_write:
mnt_drop_write(parent_path.mnt);
out_put_old:
dput(real_old);
return err;
}

static int actiondfs_unlink(struct inode *dir, struct dentry *dentry)
{
struct actiondfs_node *node = d_inode(dentry)->i_private;
Expand Down Expand Up @@ -2870,8 +2947,12 @@ static int actiondfs_rename(struct mnt_idmap *idmap, struct inode *old_dir,
rd.flags = flags;
err = vfs_rename(&rd);
if (!err) {
if (new_node)
clear_nlink(d_inode(new_dentry));
if (new_node && new_node != old_node) {
if (S_ISDIR(new_node->mode))
clear_nlink(d_inode(new_dentry));
else
drop_nlink(d_inode(new_dentry));
}
if (S_ISDIR(old_node->mode)) {
if (old_dir != new_dir) {
drop_nlink(old_dir);
Expand Down Expand Up @@ -3351,6 +3432,8 @@ static const struct file_operations actiondfs_file_fops = {
static const struct inode_operations actiondfs_dir_iops = {
.lookup = actiondfs_lookup,
.create = actiondfs_create,
.mknod = actiondfs_mknod,
.link = actiondfs_link,
.symlink = actiondfs_symlink,
.mkdir = actiondfs_mkdir,
.unlink = actiondfs_unlink,
Expand Down
78 changes: 78 additions & 0 deletions tools/e2e_action_tool.zig
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,30 @@ fn createParentDirs(io: std.Io, root: std.Io.Dir, path: []const u8) !void {
try root.createDirPath(io, path[0..slash]);
}

fn exerciseUnixSocket(io: std.Io, root: std.Io.Dir) !void {
if (@import("builtin").os.tag != .linux) return;
const linux = std.os.linux;
const socket_name = "actiond-fs-regression.sock";
var addr: linux.sockaddr.un = .{ .path = @splat(0) };
@memcpy(addr.path[0..socket_name.len], socket_name);
const server_rc = linux.socket(linux.AF.UNIX, linux.SOCK.STREAM | linux.SOCK.CLOEXEC, 0);
try requireFilesystem(linux.errno(server_rc) == .SUCCESS, "creating Unix socket failed");
const server: i32 = @intCast(server_rc);
defer _ = linux.close(server);
try requireFilesystem(linux.errno(linux.bind(server, @ptrCast(&addr), @sizeOf(linux.sockaddr.un))) == .SUCCESS, "binding pathname Unix socket failed");
defer root.deleteFile(io, socket_name) catch {};
try expectDirectoryEntry(io, root, socket_name, .unix_domain_socket, true);
try requireFilesystem(linux.errno(linux.listen(server, 1)) == .SUCCESS, "listening on Unix socket failed");
const client_rc = linux.socket(linux.AF.UNIX, linux.SOCK.STREAM | linux.SOCK.CLOEXEC, 0);
try requireFilesystem(linux.errno(client_rc) == .SUCCESS, "creating Unix client failed");
const client: i32 = @intCast(client_rc);
defer _ = linux.close(client);
try requireFilesystem(linux.errno(linux.connect(client, @ptrCast(&addr), @sizeOf(linux.sockaddr.un))) == .SUCCESS, "connecting to pathname Unix socket failed");
const accepted = linux.accept(server, null, null);
try requireFilesystem(linux.errno(accepted) == .SUCCESS, "accepting Unix socket failed");
_ = linux.close(@intCast(accepted));
}

fn exerciseFilesystem(
io: std.Io,
allocator: std.mem.Allocator,
Expand All @@ -758,6 +782,7 @@ fn exerciseFilesystem(
return filesystemFailure("filesystem output path did not include an output parent");
var iterable_root = try root.openDir(io, ".", .{ .iterate = true });
defer iterable_root.close(io);
try exerciseUnixSocket(io, iterable_root);
try requireSandboxOwnership(
iterable_root.handle,
"staged workspace root did not preserve backing filesystem ownership",
Expand All @@ -780,6 +805,7 @@ fn exerciseFilesystem(
try exerciseFileMetadata(io, dir);
try exerciseDirectoryIteration(io, dir);
try exerciseLinkCounts(io, dir);
try exerciseLargeSparseFile(io, dir);
try exerciseSymlinks(io, allocator, root, dir, inputs);
try exerciseImmutableInputAuthorization(io, allocator, root, inputs);
try syncDirectory(dir);
Expand Down Expand Up @@ -1128,7 +1154,53 @@ fn expectDirectoryEntry(
try requireFilesystem(found == expected_present, "rewound directory listing did not reflect a mutation");
}

// Reopening a disk image must retain large-file support in the backing open.
// Keep the regression sparse and remove it before uploading action outputs.
fn exerciseLargeSparseFile(io: std.Io, dir: std.Io.Dir) !void {
const size: u64 = 5 * 1024 * 1024 * 1024;
{
var file = try dir.createFile(io, "large-sparse.img", .{ .read = true });
defer file.close(io);
try file.setLength(io, size);
}
defer dir.deleteFile(io, "large-sparse.img") catch {};
var file = try dir.openFile(io, "large-sparse.img", .{ .mode = .read_write });
defer file.close(io);
try requireFilesystem((try file.stat(io)).size == size, "reopening a sparse disk image lost its size");
try file.writePositionalAll(io, "X", size - 1);
var tail: [1]u8 = undefined;
try requireFilesystem(try file.readPositionalAll(io, &tail, size - 1) == 1 and tail[0] == 'X', "large-file positional I/O failed");
}

fn exerciseLinkCounts(io: std.Io, dir: std.Io.Dir) !void {
var linked = try dir.createFile(io, "hardlink-original.txt", .{ .read = true });
defer linked.close(io);
try linked.writeStreamingAll(io, "linked payload\n");
const linked_inode = (try linked.stat(io)).inode;
try dir.hardLink("hardlink-original.txt", dir, "hardlink-alias.txt", io, .{});
try requireFilesystem((try linked.stat(io)).nlink == 2, "hard link did not increment the shared inode link count");
try requireFilesystem((try dir.statFile(io, "hardlink-alias.txt", .{})).inode == linked_inode, "hard link created a different inode");
try dir.deleteFile(io, "hardlink-original.txt");
try requireFilesystem((try linked.stat(io)).nlink == 1, "unlinking one hard link did not preserve the other link");
{
var alias = try dir.openFile(io, "hardlink-alias.txt", .{ .mode = .read_write });
defer alias.close(io);
try alias.writeStreamingAll(io, "shared payload\n");
}
var linked_buffer: [32]u8 = undefined;
const linked_size = try linked.readPositionalAll(io, &linked_buffer, 0);
try requireFilesystem(std.mem.eql(u8, linked_buffer[0..linked_size], "shared payload\n"), "writing a hard link did not update the original open file");
try dir.hardLink("hardlink-alias.txt", dir, "hardlink-last.txt", io, .{});
var replacement = try dir.createFile(io, "hardlink-replacement.txt", .{ .read = true });
defer replacement.close(io);
try dir.rename("hardlink-replacement.txt", dir, "hardlink-alias.txt", io);
try requireFilesystem((try linked.stat(io)).nlink == 1, "rename replacement removed the remaining hard link");
try dir.rename("hardlink-last.txt", dir, "hardlink-renamed.txt", io);
try requireFilesystem((try dir.statFile(io, "hardlink-renamed.txt", .{})).inode == linked_inode, "renaming a hard link changed its inode");
try dir.deleteFile(io, "hardlink-renamed.txt");
try requireFilesystem((try linked.stat(io)).nlink == 0, "unlinking the last hard link did not clear the open file link count");
try dir.deleteFile(io, "hardlink-alias.txt");

var unlinked = try dir.createFile(io, "unlink-open.txt", .{ .read = true });
defer unlinked.close(io);
try requireFilesystem((try unlinked.stat(io)).nlink == 1, "new staged file did not report one link");
Expand Down Expand Up @@ -1258,6 +1330,12 @@ fn exerciseImmutableInputAuthorization(
if ((try root.statFile(io, input, .{})).kind != .file) continue;
var file = try root.openFile(io, input, .{});
defer file.close(io);
if (root.hardLink(input, root, "immutable-hardlink-rejected", io, .{})) |_| {
return filesystemFailure("hard link unexpectedly exposed an immutable input");
} else |err| switch (err) {
error.AccessDenied, error.PermissionDenied, error.ReadOnlyFileSystem => {},
else => return err,
}
switch (std.os.linux.errno(linux.fchmod(file.handle, 0o600))) {
.PERM, .ACCES, .ROFS => {},
.SUCCESS => return filesystemFailure("chmod unexpectedly changed an immutable input"),
Expand Down
Loading