Files
Matthew Lugg 29bcd2d543 std.posix.test: fix dl_iterate_phdr test
The old test was relying on the following properties:

* There is a segment containing the ehdr, i.e. whose file offset is 0

* That segment is the first phdr with type `PT_LOAD`; because `PT_LOAD`
  segments are sorted on `p_vaddr`, this means its virtual address is
  the lowest of all loadable segments

The first assumption, although not standard, is somewhat reasonable, and
the commit prior to this one makes `Elf2` obey it. However, the second
assumption is not based on anything, and to my knowledge the linker is
entirely welcome to lay out the virtual address space as it sees fit.
Therefore, refactor this test to eliminate that bad assumption.
2026-08-16 10:37:20 +01:00

516 lines
16 KiB
Zig

const builtin = @import("builtin");
const native_os = builtin.target.os.tag;
const AtomicRmwOp = std.builtin.AtomicRmwOp;
const AtomicOrder = std.builtin.AtomicOrder;
const std = @import("../std.zig");
const Io = std.Io;
const Dir = std.Io.Dir;
const posix = std.posix;
const mem = std.mem;
const elf = std.elf;
const linux = std.os.linux;
const AT = std.posix.AT;
const testing = std.testing;
const expect = std.testing.expect;
const expectEqual = std.testing.expectEqual;
const expectEqualSlices = std.testing.expectEqualSlices;
const expectEqualStrings = std.testing.expectEqualStrings;
const expectError = std.testing.expectError;
const tmpDir = std.testing.tmpDir;
const fstest = @import("../fs/test.zig");
test "check WASI CWD" {
if (native_os == .wasi) {
const cwd: Dir = .cwd();
if (cwd.handle != 3) {
@panic("WASI code that uses cwd (like this test) needs a preopen for cwd (add '--dir=.' to wasmtime)");
}
if (!builtin.link_libc) {
// WASI without-libc hardcodes fd 3 as the FDCWD token so it can be passed directly to WASI calls
try expectEqual(3, posix.AT.FDCWD);
}
}
}
test "getuid" {
if (native_os == .windows or native_os == .wasi) return error.SkipZigTest;
_ = posix.system.getuid();
_ = posix.system.geteuid();
}
test "getgid" {
if (native_os == .windows or native_os == .wasi) return error.SkipZigTest;
_ = posix.system.getgid();
_ = posix.system.getegid();
}
test "sigaltstack" {
if (native_os == .windows or native_os == .wasi) return error.SkipZigTest;
var st: posix.stack_t = undefined;
try posix.sigaltstack(null, &st);
// Setting a stack size less than MINSIGSTKSZ returns ENOMEM
st.flags = 0;
st.size = 1;
try expectError(error.SizeTooSmall, posix.sigaltstack(&st, null));
}
// If the type is not available use void to avoid erroring out when `iter_fn` is
// analyzed
const have_dl_phdr_info = posix.system.dl_phdr_info != void;
const dl_phdr_info = if (have_dl_phdr_info) posix.dl_phdr_info else anyopaque;
const IterFnError = error{
MissingLoadSegment,
MissingEhdrLoadSegment,
BadElfMagic,
PhnumMismatch,
};
fn iter_fn(info: *dl_phdr_info, size: usize, counter: *usize) IterFnError!void {
_ = size;
// Count how many libraries are loaded
counter.* += 1;
// The image should contain at least one loadable segment
if (info.phnum < 1) return error.MissingLoadSegment;
// For some quick and dirty validation, find the phdr which contains the ELF
// header, and check it makes sense.
for (info.phdr[0..info.phnum]) |phdr| {
if (phdr.type != .LOAD) continue;
if (phdr.offset != 0) continue;
// This segment holds the ELF header at the start
const ehdr: *elf.Ehdr = @ptrFromInt(info.addr + phdr.vaddr);
if (!mem.eql(u8, ehdr.e_ident[0..4], elf.MAGIC)) return error.BadElfMagic;
if (ehdr.e_phnum != info.phnum) return error.PhnumMismatch;
break;
} else {
return error.MissingEhdrLoadSegment;
}
}
test "dl_iterate_phdr" {
if (builtin.object_format != .elf) return error.SkipZigTest;
var counter: usize = 0;
try posix.dl_iterate_phdr(&counter, IterFnError, iter_fn);
try expect(counter != 0);
}
test "gethostname" {
if (native_os == .windows or native_os == .wasi)
return error.SkipZigTest;
var buf: [posix.HOST_NAME_MAX]u8 = undefined;
const hostname = try posix.gethostname(&buf);
try expect(hostname.len != 0);
}
test "pipe" {
if (native_os == .windows or native_os == .wasi)
return error.SkipZigTest;
const io = testing.io;
const fds = try std.Io.Threaded.pipe2(.{});
const out: Io.File = .{ .handle = fds[0], .flags = .{ .nonblocking = false } };
const in: Io.File = .{ .handle = fds[1], .flags = .{ .nonblocking = false } };
try in.writeStreamingAll(io, "hello");
var buf: [16]u8 = undefined;
try expect((try out.readStreaming(io, &.{&buf})) == 5);
try expectEqualSlices(u8, buf[0..5], "hello");
out.close(io);
in.close(io);
}
test "memfd_create" {
const io = testing.io;
// memfd_create is only supported by linux and freebsd.
switch (native_os) {
.linux => {},
.freebsd => {
if (comptime builtin.os.version_range.semver.max.order(.{ .major = 13, .minor = 0, .patch = 0 }) == .lt)
return error.SkipZigTest;
},
else => return error.SkipZigTest,
}
const file: Io.File = .{
.handle = try posix.memfd_create("test", 0),
.flags = .{ .nonblocking = false },
};
defer file.close(io);
try file.writePositionalAll(io, "test", 0);
var buf: [10]u8 = undefined;
const bytes_read = try file.readPositionalAll(io, &buf, 0);
try expect(bytes_read == 4);
try expectEqualStrings("test", buf[0..4]);
}
test "mmap" {
if (native_os == .windows or native_os == .wasi)
return error.SkipZigTest;
const io = testing.io;
var tmp = tmpDir(.{});
defer tmp.cleanup();
// Simple mmap() call with non page-aligned size
{
const data = try posix.mmap(
null,
1234,
.{ .READ = true, .WRITE = true },
.{ .TYPE = .PRIVATE, .ANONYMOUS = true },
-1,
0,
);
defer posix.munmap(data);
try expectEqual(@as(usize, 1234), data.len);
// By definition the data returned by mmap is zero-filled
try expect(mem.eql(u8, data, &@as([1234]u8, @splat(0x00))));
// Make sure the memory is writeable as requested
@memset(data, 0x55);
try expect(mem.eql(u8, data, &@as([1234]u8, @splat(0x55))));
}
const test_out_file = "os_tmp_test";
// Must be a multiple of the page size so that the test works with mmap2
const alloc_size = 8 * std.heap.pageSize();
// Create a file used for testing mmap() calls with a file descriptor
{
const file = try tmp.dir.createFile(io, test_out_file, .{});
defer file.close(io);
var stream = file.writer(io, &.{});
var i: usize = 0;
while (i < alloc_size / @sizeOf(u32)) : (i += 1) {
try stream.interface.writeInt(u32, @intCast(i), .little);
}
}
// Map the whole file
{
const file = try tmp.dir.openFile(io, test_out_file, .{});
defer file.close(io);
const data = try posix.mmap(
null,
alloc_size,
.{ .READ = true },
.{ .TYPE = .PRIVATE },
file.handle,
0,
);
defer posix.munmap(data);
var stream: std.Io.Reader = .fixed(data);
var i: usize = 0;
while (i < alloc_size / @sizeOf(u32)) : (i += 1) {
try expectEqual(i, try stream.takeInt(u32, .little));
}
}
if (builtin.cpu.arch == .hexagon) return error.SkipZigTest;
// Map the upper half of the file
{
const file = try tmp.dir.openFile(io, test_out_file, .{});
defer file.close(io);
const data = try posix.mmap(
null,
alloc_size / 2,
.{ .READ = true },
.{ .TYPE = .PRIVATE },
file.handle,
alloc_size / 2,
);
defer posix.munmap(data);
var stream: std.Io.Reader = .fixed(data);
var i: usize = alloc_size / 2 / @sizeOf(u32);
while (i < alloc_size / @sizeOf(u32)) : (i += 1) {
try expectEqual(i, try stream.takeInt(u32, .little));
}
}
}
test "fcntl" {
if (native_os == .windows or native_os == .wasi)
return error.SkipZigTest;
const io = testing.io;
var tmp = tmpDir(.{});
defer tmp.cleanup();
const test_out_file = "os_tmp_test";
const file = try tmp.dir.createFile(io, test_out_file, .{});
defer file.close(io);
// Note: The test assumes createFile opens the file with CLOEXEC
{
const flags = posix.system.fcntl(file.handle, posix.F.GETFD, @as(usize, 0));
try expect((flags & posix.FD_CLOEXEC) != 0);
}
{
_ = posix.system.fcntl(file.handle, posix.F.SETFD, @as(usize, 0));
const flags = posix.system.fcntl(file.handle, posix.F.GETFD, @as(usize, 0));
try expect((flags & posix.FD_CLOEXEC) == 0);
}
{
_ = posix.system.fcntl(file.handle, posix.F.SETFD, @as(usize, posix.FD_CLOEXEC));
const flags = posix.system.fcntl(file.handle, posix.F.GETFD, @as(usize, 0));
try expect((flags & posix.FD_CLOEXEC) != 0);
}
}
test "signalfd" {
switch (native_os) {
.linux, .illumos => {},
else => return error.SkipZigTest,
}
_ = &posix.signalfd;
}
test "sync" {
if (native_os != .linux)
return error.SkipZigTest;
// Unfortunately, we cannot safely call `sync` or `syncfs`, because if file IO is happening
// than the system can commit the results to disk, such calls could block indefinitely.
_ = &posix.sync;
_ = &posix.syncfs;
}
test "fsync" {
switch (native_os) {
.linux, .illumos => {},
else => return error.SkipZigTest,
}
const io = testing.io;
var tmp = tmpDir(.{});
defer tmp.cleanup();
const test_out_file = "os_tmp_test";
const file = try tmp.dir.createFile(io, test_out_file, .{});
defer file.close(io);
try file.sync(io);
try posix.fdatasync(file.handle);
}
test "getrlimit and setrlimit" {
if (posix.system.rlimit_resource == void) return error.SkipZigTest;
inline for (@typeInfo(posix.rlimit_resource).@"enum".field_values) |field_value| {
const resource: posix.rlimit_resource = @fromBackingInt(@intCast(field_value));
const limit = try posix.getrlimit(resource);
// XNU kernel does not support RLIMIT_STACK if a custom stack is active,
// which looks to always be the case. EINVAL is returned.
// See https://github.com/apple-oss-distributions/xnu/blob/5e3eaea39dcf651e66cb99ba7d70e32cc4a99587/bsd/kern/kern_resource.c#L1173
if (native_os.isDarwin() and resource == .STACK) {
continue;
}
// On 32 bit MIPS musl includes a fix which changes limits greater than -1UL/2 to RLIM_INFINITY.
// See http://git.musl-libc.org/cgit/musl/commit/src/misc/getrlimit.c?id=8258014fd1e34e942a549c88c7e022a00445c352
//
// This happens for example if RLIMIT_MEMLOCK is bigger than ~2GiB.
// In that case the following the limit would be RLIM_INFINITY and the following setrlimit fails with EPERM.
if (builtin.cpu.arch.isMIPS() and builtin.link_libc) {
if (limit.cur != linux.RLIM.INFINITY) {
try posix.setrlimit(resource, limit);
}
} else {
try posix.setrlimit(resource, limit);
}
}
}
test "sigrtmin/max" {
if (native_os.isDarwin() or switch (native_os) {
.wasi, .windows, .openbsd, .dragonfly => true,
else => false,
}) return error.SkipZigTest;
try expect(posix.sigrtmin() >= 32);
try expect(posix.sigrtmin() >= posix.system.sigrtmin());
try expect(posix.sigrtmin() < posix.system.sigrtmax());
}
test "sigset empty/full" {
if (native_os == .wasi or native_os == .windows)
return error.SkipZigTest;
var set: posix.sigset_t = posix.sigemptyset();
for (1..posix.NSIG) |i| {
const sig = std.enums.fromInt(posix.SIG, i) orelse continue;
try expectEqual(false, posix.sigismember(&set, sig));
}
// The C library can reserve some (unnamed) signals, so can't check the full
// NSIG set is defined, but just test a couple:
set = posix.sigfillset();
try expectEqual(true, posix.sigismember(&set, .CHLD));
try expectEqual(true, posix.sigismember(&set, .INT));
}
// Some signals (i.e., 32 - 34 on glibc/musl) are not allowed to be added to a
// sigset by the C library, so avoid testing them.
fn reserved_signo(i: usize) bool {
if (native_os.isDarwin()) return false;
if (!builtin.link_libc) return false;
const max = if (native_os == .netbsd) 32 else 31;
if (i > max) return true;
if (native_os == .openbsd or native_os == .dragonfly) return false; // no RT signals
return i < posix.sigrtmin();
}
test "sigset add/del" {
if (native_os == .wasi or native_os == .windows)
return error.SkipZigTest;
var sigset: posix.sigset_t = posix.sigemptyset();
// See that none are set, then set each one, see that they're all set, then
// remove them all, and then see that none are set.
for (1..posix.NSIG) |i| {
const sig = std.enums.fromInt(posix.SIG, i) orelse continue;
try expectEqual(false, posix.sigismember(&sigset, sig));
}
for (1..posix.NSIG) |i| {
if (!reserved_signo(i)) {
const sig = std.enums.fromInt(posix.SIG, i) orelse continue;
posix.sigaddset(&sigset, sig);
}
}
for (1..posix.NSIG) |i| {
if (!reserved_signo(i)) {
const sig = std.enums.fromInt(posix.SIG, i) orelse continue;
try expectEqual(true, posix.sigismember(&sigset, sig));
}
}
for (1..posix.NSIG) |i| {
if (!reserved_signo(i)) {
const sig = std.enums.fromInt(posix.SIG, i) orelse continue;
posix.sigdelset(&sigset, sig);
}
}
for (1..posix.NSIG) |i| {
const sig = std.enums.fromInt(posix.SIG, i) orelse continue;
try expectEqual(false, posix.sigismember(&sigset, sig));
}
}
test "getpid" {
if (native_os == .wasi) return error.SkipZigTest;
if (native_os == .windows) return error.SkipZigTest;
try expect(posix.system.getpid() != 0);
}
test "getppid" {
if (native_os == .wasi) return error.SkipZigTest;
if (native_os == .windows) return error.SkipZigTest;
if (native_os == .plan9 and !builtin.link_libc) return error.SkipZigTest;
try expect(posix.getppid() >= 0);
}
test "rename smoke test" {
if (native_os == .windows) return error.SkipZigTest;
if (!fstest.isRealPathSupported()) return error.SkipZigTest;
const io = testing.io;
const gpa = testing.allocator;
var tmp = tmpDir(.{});
defer tmp.cleanup();
const base_path = try tmp.dir.realPathFileAlloc(io, ".", gpa);
defer gpa.free(base_path);
const mode: posix.mode_t = if (native_os == .windows) 0 else 0o666;
{
// Create some file using `open`.
const file_path = try Dir.path.join(gpa, &.{ base_path, "some_file" });
defer gpa.free(file_path);
const file = try Io.Dir.cwd().createFile(io, file_path, .{
.read = true,
.exclusive = true,
.permissions = .fromMode(mode),
});
file.close(io);
// Rename the file
const new_file_path = try Dir.path.join(gpa, &.{ base_path, "some_other_file" });
defer gpa.free(new_file_path);
try Io.Dir.renameAbsolute(file_path, new_file_path, io);
}
{
// Try opening renamed file
const file_path = try Dir.path.join(gpa, &.{ base_path, "some_other_file" });
defer gpa.free(file_path);
const file = try Io.Dir.cwd().openFile(io, file_path, .{ .mode = .read_write });
file.close(io);
}
{
// Try opening original file - should fail with error.FileNotFound
const file_path = try Dir.path.join(gpa, &.{ base_path, "some_file" });
defer gpa.free(file_path);
try expectError(error.FileNotFound, Io.Dir.cwd().openFile(io, file_path, .{ .mode = .read_write }));
}
{
// Create some directory
const file_path = try Dir.path.join(gpa, &.{ base_path, "some_dir" });
defer gpa.free(file_path);
try Io.Dir.createDirAbsolute(io, file_path, .fromMode(mode));
// Rename the directory
const new_file_path = try Dir.path.join(gpa, &.{ base_path, "some_other_dir" });
defer gpa.free(new_file_path);
try Io.Dir.renameAbsolute(file_path, new_file_path, io);
}
{
// Try opening renamed directory
const file_path = try Dir.path.join(gpa, &.{ base_path, "some_other_dir" });
defer gpa.free(file_path);
const dir = try Io.Dir.cwd().openDir(io, file_path, .{});
dir.close(io);
}
{
// Try opening original directory - should fail with error.FileNotFound
const file_path = try Dir.path.join(gpa, &.{ base_path, "some_dir" });
defer gpa.free(file_path);
try expectError(error.FileNotFound, Io.Dir.cwd().openDir(io, file_path, .{}));
}
}