-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Fix the damn deadlock #7553
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix the damn deadlock #7553
Changes from all commits
c7834f2
c452bb1
ed39ff2
3f9588c
3366e4c
cb290ed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -19,6 +19,7 @@ const builtin = @import("builtin"); | |||||
const Os = builtin.Os; | ||||||
const TailQueue = std.TailQueue; | ||||||
const maxInt = std.math.maxInt; | ||||||
const assert = std.debug.assert; | ||||||
|
||||||
pub const ChildProcess = struct { | ||||||
pid: if (builtin.os.tag == .windows) void else i32, | ||||||
|
@@ -376,19 +377,44 @@ pub const ChildProcess = struct { | |||||
if (any_ignore) os.close(dev_null_fd); | ||||||
} | ||||||
|
||||||
var env_map_owned: BufMap = undefined; | ||||||
var we_own_env_map: bool = undefined; | ||||||
const env_map = if (self.env_map) |env_map| x: { | ||||||
we_own_env_map = false; | ||||||
break :x env_map; | ||||||
} else x: { | ||||||
we_own_env_map = true; | ||||||
env_map_owned = try process.getEnvMap(self.allocator); | ||||||
break :x &env_map_owned; | ||||||
var arena_allocator = std.heap.ArenaAllocator.init(self.allocator); | ||||||
defer arena_allocator.deinit(); | ||||||
const arena = &arena_allocator.allocator; | ||||||
|
||||||
// The POSIX standard does not allow malloc() between fork() and execve(), | ||||||
// and `self.allocator` may be a libc allocator. | ||||||
// I have personally observed the child process deadlocking when it tries | ||||||
// to call malloc() due to a heap allocation between fork() and execve(), | ||||||
// in musl v1.1.24. | ||||||
// Additionally, we want to reduce the number of possible ways things | ||||||
// can fail between fork() and execve(). | ||||||
// Therefore, we do all the allocation for the execve() before the fork(). | ||||||
// This means we must do the null-termination of argv and env vars here. | ||||||
const argv_buf = try arena.alloc(?[*:0]u8, self.argv.len + 1); | ||||||
for (self.argv) |arg, i| { | ||||||
const arg_buf = try arena.alloc(u8, arg.len + 1); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could use
Suggested change
|
||||||
@memcpy(arg_buf.ptr, arg.ptr, arg.len); | ||||||
arg_buf[arg.len] = 0; | ||||||
argv_buf[i] = arg_buf[0..arg.len :0].ptr; | ||||||
} | ||||||
argv_buf[self.argv.len] = null; | ||||||
const argv_ptr = argv_buf[0..self.argv.len :null].ptr; | ||||||
|
||||||
const envp = m: { | ||||||
if (self.env_map) |env_map| { | ||||||
const envp_buf = try createNullDelimitedEnvMap(arena, env_map); | ||||||
break :m envp_buf.ptr; | ||||||
} else if (std.builtin.link_libc) { | ||||||
break :m std.c.environ; | ||||||
} else if (std.builtin.output_mode == .Exe) { | ||||||
// Then we have Zig start code and this works. | ||||||
// TODO type-safety for null-termination of `os.environ`. | ||||||
break :m @ptrCast([*:null]?[*:0]u8, os.environ.ptr); | ||||||
} else { | ||||||
// TODO come up with a solution for this. | ||||||
@compileError("missing std lib enhancement: ChildProcess implementation has no way to collect the environment variables to forward to the child process"); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This has the upshot of not allowing Linux libraries not linking libc to use const std = @import("std")
fn IterThingOnce(Thing: anytype) type {
return struct {
yielded: bool = false,
optional_thing: ?Thing,
pub fn next(self: *@This()) ?Thing {
if (self.optional_thing) |thing| {
if (self.yielded) {
return null;
} else {
defer self.yielded = true;
return thing;
}
} else if (false) {
@compileError("This can't happen since `false` is a comptime known literal.");
} else {
// Compiles and test passes by exchanging commented out line below.
@compileError("This seems like it should not happen.");
// return null;
}
}
};
}
test "Compiler Bug?" {
var example: IterThingOnce([]const u8) = .{.optional_thing = "Slatibartfast"};
try std.testing.expectEqual("Slatibartfast", example.next().?);
try std.testing.expect(example.next() == null);
} ... error: This seems like it should not happen.
@compileError("This seems like it should not happen.");
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ But, on reflection, that's the point of the else arm triggering a compile error - that branch can be conditionally hit at runtime. I guess what I'm wondering is if that was the intention; i.e.: Not allow the Linux library with no libc case to get a runtime error instead iff they do not set up an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please file an issue about this; it's very likely to get lost on a 4 years old PR. It seems like a case we should fix. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Aha - ok, excellent. I'll do so. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
} | ||||||
}; | ||||||
defer { | ||||||
if (we_own_env_map) env_map_owned.deinit(); | ||||||
} | ||||||
|
||||||
// This pipe is used to communicate errors between the time of fork | ||||||
// and execve from the child process to the parent process. | ||||||
|
@@ -438,7 +464,10 @@ pub const ChildProcess = struct { | |||||
os.setreuid(uid, uid) catch |err| forkChildErrReport(err_pipe[1], err); | ||||||
} | ||||||
|
||||||
const err = os.execvpe_expandArg0(self.allocator, self.expand_arg0, self.argv, env_map); | ||||||
const err = switch (self.expand_arg0) { | ||||||
.expand => os.execvpeZ_expandArg0(.expand, argv_buf.ptr[0].?, argv_ptr, envp), | ||||||
.no_expand => os.execvpeZ_expandArg0(.no_expand, argv_buf.ptr[0].?, argv_ptr, envp), | ||||||
}; | ||||||
forkChildErrReport(err_pipe[1], err); | ||||||
} | ||||||
|
||||||
|
@@ -881,3 +910,24 @@ pub fn createWindowsEnvBlock(allocator: *mem.Allocator, env_map: *const BufMap) | |||||
i += 1; | ||||||
return allocator.shrink(result, i); | ||||||
} | ||||||
|
||||||
pub fn createNullDelimitedEnvMap(arena: *mem.Allocator, env_map: *const std.BufMap) ![:null]?[*:0]u8 { | ||||||
const envp_count = env_map.count(); | ||||||
const envp_buf = try arena.alloc(?[*:0]u8, envp_count + 1); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
mem.set(?[*:0]u8, envp_buf, null); | ||||||
{ | ||||||
var it = env_map.iterator(); | ||||||
var i: usize = 0; | ||||||
while (it.next()) |pair| : (i += 1) { | ||||||
const env_buf = try arena.alloc(u8, pair.key.len + pair.value.len + 2); | ||||||
@memcpy(env_buf.ptr, pair.key.ptr, pair.key.len); | ||||||
env_buf[pair.key.len] = '='; | ||||||
@memcpy(env_buf.ptr + pair.key.len + 1, pair.value.ptr, pair.value.len); | ||||||
const len = env_buf.len - 1; | ||||||
env_buf[len] = 0; | ||||||
envp_buf[i] = env_buf[0..len :0].ptr; | ||||||
} | ||||||
assert(i == envp_count); | ||||||
} | ||||||
return envp_buf[0..envp_count :null]; | ||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.