authorgravatar for bratishkaerik@landless-city.netEric Joldasov <bratishkaerik@landless-city.net> 2024-09-28 23:32:12+05:00
committergravatar for bratishkaerik@landless-city.netEric Joldasov <bratishkaerik@landless-city.net> 2025-01-20 14:29:04+05:00
loga79e421e59ef4cc9883437551fdcfcef611d501c
tree6a8c5d772e861d24d8bca2fdceac50cb7a73dc0a
parent5bbf3f55616cf4b021fecacd41b73bfea8ab953f
signaturelock-open Commit is signed but in an unrecognized format.

std.zig.system: use both PATH and hardcoded locations to find `env`

Should help systems that have main `env` binary in different location than hardcoded `/usr/bin/env` **during build** (not neccessarily always), like Nix/Guix, Termux, Gentoo Prefix etc. Related: https://www.github.com/ziglang/zig/issues/12156 https://www.github.com/ziglang/zig/issues/14146 https://www.github.com/ziglang/zig/issues/14577 https://www.github.com/ziglang/zig/issues/15898 Source for logic: https://www.github.com/ziglang/zig/issues/14146#issuecomment-2308984936 Signed-off-by: Eric Joldasov <bratishkaerik@landless-city.net>

1 files changed, 139 insertions(+), 108 deletions(-)

lib/std/zig/system.zig+139-108
...@@ -995,6 +995,106 @@ fn glibcVerFromSoFile(file: fs.File) !std.SemanticVersion {...@@ -995,6 +995,106 @@ fn glibcVerFromSoFile(file: fs.File) !std.SemanticVersion {
995 return max_ver;995 return max_ver;
996}996}
997997
998/// This functions tries to open file located at `start_path`, and then guesses
999/// whether it is a script or an ELF file.
1000///
1001/// If it finds "shebang line", file is considered a script, and logic is re-run
1002/// using interpreter referenced after "#!" symbols. If interpreter is itself also a script,
1003/// logic becomes recursive until non-script file is found.
1004///
1005/// If it finds ELF magic sequence, file is considered an ELF file and function returns.
1006fn resolveElfFileRecursively(cwd: fs.Dir, start_path: []const u8) error{UnableToFindElfFile}!fs.File {
1007 var current_path = start_path;
1008
1009 // According to `man 2 execve`:
1010 //
1011 // The kernel imposes a maximum length on the text
1012 // that follows the "#!" characters at the start of a script;
1013 // characters beyond the limit are ignored.
1014 // Before Linux 5.1, the limit is 127 characters.
1015 // Since Linux 5.1, the limit is 255 characters.
1016 //
1017 // Tests show that bash and zsh consider 255 as total limit,
1018 // *including* "#!" characters and ignoring newline.
1019 // For safety, we set max length as 255 + \n (1).
1020 var buffer: [255 + 1]u8 = undefined;
1021 while (true) {
1022 // Interpreter path can be relative on Linux, but
1023 // for simplicity we are asserting it is an absolute path.
1024 assert(std.fs.path.isAbsolute(current_path));
1025 const file = cwd.openFile(current_path, .{}) catch |err| switch (err) {
1026 error.NoSpaceLeft => unreachable,
1027 error.NameTooLong => unreachable,
1028 error.PathAlreadyExists => unreachable,
1029 error.SharingViolation => unreachable,
1030 error.InvalidUtf8 => unreachable, // WASI only
1031 error.InvalidWtf8 => unreachable, // Windows only
1032 error.BadPathName => unreachable,
1033 error.PipeBusy => unreachable,
1034 error.FileLocksNotSupported => unreachable,
1035 error.WouldBlock => unreachable,
1036 error.FileBusy => unreachable, // opened without write permissions
1037 error.AntivirusInterference => unreachable, // Windows-only error
1038
1039 error.IsDir,
1040 error.NotDir,
1041
1042 error.AccessDenied,
1043 error.DeviceBusy,
1044 error.FileTooBig,
1045 error.SymLinkLoop,
1046 error.ProcessFdQuotaExceeded,
1047 error.SystemFdQuotaExceeded,
1048 error.SystemResources,
1049
1050 error.FileNotFound,
1051 error.NetworkNotFound,
1052 error.NoDevice,
1053 error.Unexpected,
1054 => return error.UnableToFindElfFile,
1055 };
1056 var is_elf_file = false;
1057 defer if (is_elf_file == false) file.close();
1058
1059 // Shortest working interpreter path is "#!/i" (4)
1060 // (interpreter is "/i", assuming all paths are absolute, like in above comment).
1061 // ELF magic number length is also 4.
1062 //
1063 // If file is shorter than that, it is definitely not ELF file
1064 // nor file with "shebang" line.
1065 const min_len = 4;
1066
1067 const len = preadAtLeast(file, &buffer, 0, min_len) catch return error.UnableToFindElfFile;
1068 const content = buffer[0..len];
1069
1070 if (mem.eql(u8, content[0..4], std.elf.MAGIC)) {
1071 // It is very likely ELF file!
1072 is_elf_file = true;
1073 return file;
1074 } else if (mem.eql(u8, content[0..2], "#!")) {
1075 // We detected shebang, now parse entire line.
1076
1077 // Trim leading "#!", spaces and tabs.
1078 const trimmed_line = mem.trimLeft(u8, content[2..], &.{ ' ', '\t' });
1079
1080 // This line can have:
1081 // * Interpreter path only,
1082 // * Interpreter path and arguments, all separated by space, tab or NUL character.
1083 // And optionally newline at the end.
1084 const path_maybe_args = mem.trimRight(u8, trimmed_line, "\n");
1085
1086 // Separate path and args.
1087 const path_end = mem.indexOfAny(u8, path_maybe_args, &.{ ' ', '\t', 0 }) orelse path_maybe_args.len;
1088
1089 current_path = path_maybe_args[0..path_end];
1090 continue;
1091 } else {
1092 // Not a ELF file, not a shell script with "shebang line", invalid duck.
1093 return error.UnableToFindElfFile;
1094 }
1095 }
1096}
1097
998/// In the past, this function attempted to use the executable's own binary if it was dynamically1098/// In the past, this function attempted to use the executable's own binary if it was dynamically
999/// linked to answer both the C ABI question and the dynamic linker question. However, this1099/// linked to answer both the C ABI question and the dynamic linker question. However, this
1000/// could be problematic on a system that uses a RUNPATH for the compiler binary, locking1100/// could be problematic on a system that uses a RUNPATH for the compiler binary, locking
...@@ -1003,11 +1103,14 @@ fn glibcVerFromSoFile(file: fs.File) !std.SemanticVersion {...@@ -1003,11 +1103,14 @@ fn glibcVerFromSoFile(file: fs.File) !std.SemanticVersion {
1003/// the dynamic linker will match that of the compiler binary. Executables with these versions1103/// the dynamic linker will match that of the compiler binary. Executables with these versions
1004/// mismatching will fail to run.1104/// mismatching will fail to run.
1005///1105///
1006/// Therefore, this function works the same regardless of whether the compiler binary is1106/// Therefore, this function now does not inspect the executable's own binary.
1007/// dynamically or statically linked. It inspects `/usr/bin/env` as an ELF file to find the1107/// Instead, it tries to find `env` program in PATH or in hardcoded location, and uses it
1008/// answer to these questions, or if there is a shebang line, then it chases the referenced1108/// to find suitable ELF file. If `env` program is an executable, work is done and function starts to
1009/// file recursively. If that does not provide the answer, then the function falls back to1109/// inspect inner structure of a file. But if `env` is a script or other non-ELF file, it uses
1010/// defaults.1110/// interpreter path instead and tries to search ELF file again, going recursively in case interpreter
1111/// is also a script/non-ELF file.
1112///
1113/// If nothing was found, then the function falls back to defaults.
1011fn detectAbiAndDynamicLinker(1114fn detectAbiAndDynamicLinker(
1012 cpu: Target.Cpu,1115 cpu: Target.Cpu,
1013 os: Target.Os,1116 os: Target.Os,
...@@ -1075,14 +1178,33 @@ fn detectAbiAndDynamicLinker(...@@ -1075,14 +1178,33 @@ fn detectAbiAndDynamicLinker(
10751178
1076 const ld_info_list = ld_info_list_buffer[0..ld_info_list_len];1179 const ld_info_list = ld_info_list_buffer[0..ld_info_list_len];
10771180
1078 // Best case scenario: the executable is dynamically linked, and we can iterate1181 const cwd = std.fs.cwd();
1079 // over our own shared objects and find a dynamic linker.1182
1080 const elf_file = elf_file: {1183 // Algorithm is:
1081 // This block looks for a shebang line in /usr/bin/env,1184 // 1a) try_path: If PATH is non-empty and `env` file was found in one of the directories, use that.
1082 // if it finds one, then instead of using /usr/bin/env as the ELF file to examine, it uses the file it references instead,1185 // 1b) try_path: If `env` was not found or PATH is empty, try hardcoded path below.
1083 // doing the same logic recursively in case it finds another shebang line.1186 // 2a) try_hardcoded: If `env` was found in hardcoded location, use that.
1187 // 2b) try_hardcoded: If `env` was not found, fall back to default ABI and dynamic linker.
1188 // Source: https://github.com/ziglang/zig/issues/14146#issuecomment-2308984936
1189 const elf_file = (try_path: {
1190 const PATH = std.posix.getenv("PATH") orelse break :try_path null;
1191 var it = mem.tokenizeScalar(u8, PATH, fs.path.delimiter);
1192
1193 var buf: [std.fs.max_path_bytes + 1]u8 = undefined;
1194 var fbs: std.heap.FixedBufferAllocator = .init(&buf);
1195 const allocator = fbs.allocator();
1196
1197 while (it.next()) |path| : (fbs.reset()) {
1198 const start_path = std.fs.path.joinZ(allocator, &.{ path, "env" }) catch |err| switch (err) {
1199 error.OutOfMemory => continue,
1200 };
10841201
1085 var file_name: []const u8 = switch (os.tag) {1202 break :try_path resolveElfFileRecursively(cwd, start_path) catch |err| switch (err) {
1203 error.UnableToFindElfFile => continue,
1204 };
1205 } else break :try_path null;
1206 } orelse try_hardcoded: {
1207 const hardcoded_file_name = switch (os.tag) {
1086 // Since /usr/bin/env is hard-coded into the shebang line of many portable scripts, it's a1208 // Since /usr/bin/env is hard-coded into the shebang line of many portable scripts, it's a
1087 // reasonably reliable path to start with.1209 // reasonably reliable path to start with.
1088 else => "/usr/bin/env",1210 else => "/usr/bin/env",
...@@ -1090,98 +1212,10 @@ fn detectAbiAndDynamicLinker(...@@ -1090,98 +1212,10 @@ fn detectAbiAndDynamicLinker(
1090 .haiku => "/bin/env",1212 .haiku => "/bin/env",
1091 };1213 };
10921214
1093 // According to `man 2 execve`:1215 break :try_hardcoded resolveElfFileRecursively(cwd, hardcoded_file_name) catch |err| switch (err) {
1094 //1216 error.UnableToFindElfFile => null,
1095 // The kernel imposes a maximum length on the text1217 };
1096 // that follows the "#!" characters at the start of a script;1218 }) orelse return defaultAbiAndDynamicLinker(cpu, os, query);
1097 // characters beyond the limit are ignored.
1098 // Before Linux 5.1, the limit is 127 characters.
1099 // Since Linux 5.1, the limit is 255 characters.
1100 //
1101 // Tests show that bash and zsh consider 255 as total limit,
1102 // *including* "#!" characters and ignoring newline.
1103 // For safety, we set max length as 255 + \n (1).
1104 var buffer: [255 + 1]u8 = undefined;
1105 while (true) {
1106 // Interpreter path can be relative on Linux, but
1107 // for simplicity we are asserting it is an absolute path.
1108 const file = fs.openFileAbsolute(file_name, .{}) catch |err| switch (err) {
1109 error.NoSpaceLeft => unreachable,
1110 error.NameTooLong => unreachable,
1111 error.PathAlreadyExists => unreachable,
1112 error.SharingViolation => unreachable,
1113 error.InvalidUtf8 => unreachable, // WASI only
1114 error.InvalidWtf8 => unreachable, // Windows only
1115 error.BadPathName => unreachable,
1116 error.PipeBusy => unreachable,
1117 error.FileLocksNotSupported => unreachable,
1118 error.WouldBlock => unreachable,
1119 error.FileBusy => unreachable, // opened without write permissions
1120 error.AntivirusInterference => unreachable, // Windows-only error
1121
1122 error.IsDir,
1123 error.NotDir,
1124 error.AccessDenied,
1125 error.NoDevice,
1126 error.FileNotFound,
1127 error.NetworkNotFound,
1128 error.FileTooBig,
1129 error.Unexpected,
1130 => |e| {
1131 std.log.warn("Encountered error: {s}, falling back to default ABI and dynamic linker.", .{@errorName(e)});
1132 return defaultAbiAndDynamicLinker(cpu, os, query);
1133 },
1134
1135 else => |e| return e,
1136 };
1137 var is_elf_file = false;
1138 defer if (is_elf_file == false) file.close();
1139
1140 // Shortest working interpreter path is "#!/i" (4)
1141 // (interpreter is "/i", assuming all paths are absolute, like in above comment).
1142 // ELF magic number length is also 4.
1143 //
1144 // If file is shorter than that, it is definitely not ELF file
1145 // nor file with "shebang" line.
1146 const min_len: usize = 4;
1147
1148 const len = preadAtLeast(file, &buffer, 0, min_len) catch |err| switch (err) {
1149 error.UnexpectedEndOfFile,
1150 error.UnableToReadElfFile,
1151 error.ProcessNotFound,
1152 => return defaultAbiAndDynamicLinker(cpu, os, query),
1153
1154 else => |e| return e,
1155 };
1156 const content = buffer[0..len];
1157
1158 if (mem.eql(u8, content[0..4], std.elf.MAGIC)) {
1159 // It is very likely ELF file!
1160 is_elf_file = true;
1161 break :elf_file file;
1162 } else if (mem.eql(u8, content[0..2], "#!")) {
1163 // We detected shebang, now parse entire line.
1164
1165 // Trim leading "#!", spaces and tabs.
1166 const trimmed_line = mem.trimLeft(u8, content[2..], &.{ ' ', '\t' });
1167
1168 // This line can have:
1169 // * Interpreter path only,
1170 // * Interpreter path and arguments, all separated by space, tab or NUL character.
1171 // And optionally newline at the end.
1172 const path_maybe_args = mem.trimRight(u8, trimmed_line, "\n");
1173
1174 // Separate path and args.
1175 const path_end = mem.indexOfAny(u8, path_maybe_args, &.{ ' ', '\t', 0 }) orelse path_maybe_args.len;
1176
1177 file_name = path_maybe_args[0..path_end];
1178 continue;
1179 } else {
1180 // Not a ELF file, not a shell script with "shebang line", invalid duck.
1181 return defaultAbiAndDynamicLinker(cpu, os, query);
1182 }
1183 }
1184 };
1185 defer elf_file.close();1219 defer elf_file.close();
11861220
1187 // TODO: inline this function and combine the buffer we already read above to find1221 // TODO: inline this function and combine the buffer we already read above to find
...@@ -1205,10 +1239,7 @@ fn detectAbiAndDynamicLinker(...@@ -1205,10 +1239,7 @@ fn detectAbiAndDynamicLinker(
1205 error.UnexpectedEndOfFile,1239 error.UnexpectedEndOfFile,
1206 error.NameTooLong,1240 error.NameTooLong,
1207 // Finally, we fall back on the standard path.1241 // Finally, we fall back on the standard path.
1208 => |e| {1242 => defaultAbiAndDynamicLinker(cpu, os, query),
1209 std.log.warn("Encountered error: {s}, falling back to default ABI and dynamic linker.", .{@errorName(e)});
1210 return defaultAbiAndDynamicLinker(cpu, os, query);
1211 },
1212 };1243 };
1213}1244}
12141245