| ... | @@ -995,106 +995,6 @@ fn glibcVerFromSoFile(file: fs.File) !std.SemanticVersion { | ... | @@ -995,106 +995,6 @@ fn glibcVerFromSoFile(file: fs.File) !std.SemanticVersion { |
| 995 | return max_ver; | 995 | return max_ver; |
| 996 | } | 996 | } |
| 997 | | 997 | |
| 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. | | |
| 1006 | fn 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 | | | |
| 1098 | /// In the past, this function attempted to use the executable's own binary if it was dynamically | 998 | /// In the past, this function attempted to use the executable's own binary if it was dynamically |
| 1099 | /// linked to answer both the C ABI question and the dynamic linker question. However, this | 999 | /// linked to answer both the C ABI question and the dynamic linker question. However, this |
| 1100 | /// could be problematic on a system that uses a RUNPATH for the compiler binary, locking | 1000 | /// could be problematic on a system that uses a RUNPATH for the compiler binary, locking |
| ... | @@ -1103,14 +1003,11 @@ fn resolveElfFileRecursively(cwd: fs.Dir, start_path: []const u8) error{UnableTo | ... | @@ -1103,14 +1003,11 @@ fn resolveElfFileRecursively(cwd: fs.Dir, start_path: []const u8) error{UnableTo |
| 1103 | /// the dynamic linker will match that of the compiler binary. Executables with these versions | 1003 | /// the dynamic linker will match that of the compiler binary. Executables with these versions |
| 1104 | /// mismatching will fail to run. | 1004 | /// mismatching will fail to run. |
| 1105 | /// | 1005 | /// |
| 1106 | /// Therefore, this function now does not inspect the executable's own binary. | 1006 | /// Therefore, this function works the same regardless of whether the compiler binary is |
| 1107 | /// Instead, it tries to find `env` program in PATH or in hardcoded location, and uses it | 1007 | /// dynamically or statically linked. It inspects `/usr/bin/env` as an ELF file to find the |
| 1108 | /// to find suitable ELF file. If `env` program is an executable, work is done and function starts to | 1008 | /// answer to these questions, or if there is a shebang line, then it chases the referenced |
| 1109 | /// inspect inner structure of a file. But if `env` is a script or other non-ELF file, it uses | 1009 | /// file recursively. If that does not provide the answer, then the function falls back to |
| 1110 | /// interpreter path instead and tries to search ELF file again, going recursively in case interpreter | 1010 | /// defaults. |
| 1111 | /// is also a script/non-ELF file. | | |
| 1112 | /// | | |
| 1113 | /// If nothing was found, then the function falls back to defaults. | | |
| 1114 | fn detectAbiAndDynamicLinker( | 1011 | fn detectAbiAndDynamicLinker( |
| 1115 | cpu: Target.Cpu, | 1012 | cpu: Target.Cpu, |
| 1116 | os: Target.Os, | 1013 | os: Target.Os, |
| ... | @@ -1178,33 +1075,14 @@ fn detectAbiAndDynamicLinker( | ... | @@ -1178,33 +1075,14 @@ fn detectAbiAndDynamicLinker( |
| 1178 | | 1075 | |
| 1179 | const ld_info_list = ld_info_list_buffer[0..ld_info_list_len]; | 1076 | const ld_info_list = ld_info_list_buffer[0..ld_info_list_len]; |
| 1180 | | 1077 | |
| 1181 | const cwd = std.fs.cwd(); | 1078 | // Best case scenario: the executable is dynamically linked, and we can iterate |
| 1182 | | 1079 | // over our own shared objects and find a dynamic linker. |
| 1183 | // Algorithm is: | 1080 | const elf_file = elf_file: { |
| 1184 | // 1a) try_path: If PATH is non-empty and `env` file was found in one of the directories, use that. | 1081 | // This block looks for a shebang line in /usr/bin/env, |
| 1185 | // 1b) try_path: If `env` was not found or PATH is empty, try hardcoded path below. | 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, |
| 1186 | // 2a) try_hardcoded: If `env` was found in hardcoded location, use that. | 1083 | // doing the same logic recursively in case it finds another shebang line. |
| 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 | }; | | |
| 1201 | | 1084 | |
| 1202 | break :try_path resolveElfFileRecursively(cwd, start_path) catch |err| switch (err) { | 1085 | var file_name: []const u8 = switch (os.tag) { |
| 1203 | error.UnableToFindElfFile => continue, | | |
| 1204 | }; | | |
| 1205 | } else break :try_path null; | | |
| 1206 | } orelse try_hardcoded: { | | |
| 1207 | const hardcoded_file_name = switch (os.tag) { | | |
| 1208 | // Since /usr/bin/env is hard-coded into the shebang line of many portable scripts, it's a | 1086 | // Since /usr/bin/env is hard-coded into the shebang line of many portable scripts, it's a |
| 1209 | // reasonably reliable path to start with. | 1087 | // reasonably reliable path to start with. |
| 1210 | else => "/usr/bin/env", | 1088 | else => "/usr/bin/env", |
| ... | @@ -1212,10 +1090,98 @@ fn detectAbiAndDynamicLinker( | ... | @@ -1212,10 +1090,98 @@ fn detectAbiAndDynamicLinker( |
| 1212 | .haiku => "/bin/env", | 1090 | .haiku => "/bin/env", |
| 1213 | }; | 1091 | }; |
| 1214 | | 1092 | |
| 1215 | break :try_hardcoded resolveElfFileRecursively(cwd, hardcoded_file_name) catch |err| switch (err) { | 1093 | // According to `man 2 execve`: |
| 1216 | error.UnableToFindElfFile => null, | 1094 | // |
| 1217 | }; | 1095 | // The kernel imposes a maximum length on the text |
| 1218 | }) orelse return defaultAbiAndDynamicLinker(cpu, os, query); | 1096 | // that follows the "#!" characters at the start of a script; |
| | 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 | }; |
| 1219 | defer elf_file.close(); | 1185 | defer elf_file.close(); |
| 1220 | | 1186 | |
| 1221 | // TODO: inline this function and combine the buffer we already read above to find | 1187 | // TODO: inline this function and combine the buffer we already read above to find |
| ... | @@ -1239,7 +1205,10 @@ fn detectAbiAndDynamicLinker( | ... | @@ -1239,7 +1205,10 @@ fn detectAbiAndDynamicLinker( |
| 1239 | error.UnexpectedEndOfFile, | 1205 | error.UnexpectedEndOfFile, |
| 1240 | error.NameTooLong, | 1206 | error.NameTooLong, |
| 1241 | // Finally, we fall back on the standard path. | 1207 | // Finally, we fall back on the standard path. |
| 1242 | => defaultAbiAndDynamicLinker(cpu, os, query), | 1208 | => |e| { |
| | 1209 | std.log.warn("Encountered error: {s}, falling back to default ABI and dynamic linker.", .{@errorName(e)}); |
| | 1210 | return defaultAbiAndDynamicLinker(cpu, os, query); |
| | 1211 | }, |
| 1243 | }; | 1212 | }; |
| 1244 | } | 1213 | } |
| 1245 | | 1214 | |