| ... | @@ -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 | } |
| 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 | |
| 998 | /// In the past, this function attempted to use the executable's own binary if it was dynamically | 1098 | /// 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, this | 1099 | /// 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, locking | 1100 | /// 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 versions | 1103 | /// 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 is | 1106 | /// 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 the | 1107 | /// 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 referenced | 1108 | /// 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 to | 1109 | /// 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. |
| 1011 | fn detectAbiAndDynamicLinker( | 1114 | fn 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( |
| 1075 | | 1178 | |
| 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]; |
| 1077 | | 1180 | |
| 1078 | // Best case scenario: the executable is dynamically linked, and we can iterate | 1181 | 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 | }; |
| 1084 | | 1201 | |
| 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 a | 1208 | // 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 | }; |
| 1092 | | 1214 | |
| 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 text | 1217 | }; |
| 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(); |
| 1186 | | 1220 | |
| 1187 | // TODO: inline this function and combine the buffer we already read above to find | 1221 | // 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 | } |
| 1214 | | 1245 | |