@@ -995,6 +995,106 @@ fn glibcVerFromSoFile(file: fs.File) !std.SemanticVersion {
995995 return max_ver ;
996996}
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.
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+
9981098/// In the past, this function attempted to use the executable's own binary if it was dynamically
9991099/// linked to answer both the C ABI question and the dynamic linker question. However, this
10001100/// 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 {
10031103/// the dynamic linker will match that of the compiler binary. Executables with these versions
10041104/// mismatching will fail to run.
10051105///
1006- /// Therefore, this function works the same regardless of whether the compiler binary is
1007- /// dynamically or statically linked. It inspects `/usr/bin/env` as an ELF file to find the
1008- /// answer to these questions, or if there is a shebang line, then it chases the referenced
1009- /// file recursively. If that does not provide the answer, then the function falls back to
1010- /// defaults.
1106+ /// Therefore, this function now does not inspect the executable's own binary.
1107+ /// Instead, it tries to find `env` program in PATH or in hardcoded location, and uses it
1108+ /// to find suitable ELF file. If `env` program is an executable, work is done and function starts to
1109+ /// inspect inner structure of a file. But if `env` is a script or other non-ELF file, it uses
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.
10111114fn detectAbiAndDynamicLinker (
10121115 cpu : Target.Cpu ,
10131116 os : Target.Os ,
@@ -1075,113 +1178,44 @@ fn detectAbiAndDynamicLinker(
10751178
10761179 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 iterate
1079- // over our own shared objects and find a dynamic linker.
1080- const elf_file = elf_file : {
1081- // This block looks for a shebang line in /usr/bin/env,
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,
1083- // doing the same logic recursively in case it finds another shebang line.
1181+ const cwd = std .fs .cwd ();
1182+
1183+ // Algorithm is:
1184+ // 1a) try_path: If PATH is non-empty and `env` file was found in one of the directories, use that.
1185+ // 1b) try_path: If `env` was not found or PATH is empty, try hardcoded path below.
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 ) {
10861208 // Since /usr/bin/env is hard-coded into the shebang line of many portable scripts, it's a
10871209 // reasonably reliable path to start with.
10881210 else = > "/usr/bin/env" ,
10891211 // Haiku does not have a /usr root directory.
10901212 .haiku = > "/bin/env" ,
10911213 };
10921214
1093- // According to `man 2 execve`:
1094- //
1095- // The kernel imposes a maximum length on the text
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- };
1215+ break :try_hardcoded resolveElfFileRecursively (cwd , hardcoded_file_name ) catch | err | switch (err ) {
1216+ error .UnableToFindElfFile = > null ,
1217+ };
1218+ }) orelse return defaultAbiAndDynamicLinker (cpu , os , query );
11851219 defer elf_file .close ();
11861220
11871221 // TODO: inline this function and combine the buffer we already read above to find
@@ -1205,10 +1239,7 @@ fn detectAbiAndDynamicLinker(
12051239 error .UnexpectedEndOfFile ,
12061240 error .NameTooLong ,
12071241 // Finally, we fall back on the standard path.
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- },
1242+ = > defaultAbiAndDynamicLinker (cpu , os , query ),
12121243 };
12131244}
12141245
0 commit comments