Pure Erlang implementation of 9p2000 protocol
filesystem fs 9p2000 erlang 9p

Fix deadlock in unfs

This was happening when Erlang wanted to read some data about paths
(file info and listing directory) from the FS that was exported by the
same ERTS instance. It happened because of the fact, that there is
single file server shared among all processes, so these calls were
deadlocking.

hauleth.dev c74dc1e5 d72fd0b3

verified
+66 -28
+18 -6
src/e9p_unfs.erl
··· 19 19 % Create QID and Stat data for given path. 20 20 qid(Root, Path) -> 21 21 FullPath = filename:join([Root] ++ Path), 22 - case file:read_file_info(FullPath, [{time, posix}]) of 22 + case file:read_file_info(FullPath, [{time, posix}, raw]) of 23 23 {ok, #file_info{type = Type, inode = Inode} = FI} -> 24 24 QID = e9p:make_qid(Type, 0, Inode), 25 25 Stat = file_info_to_stat(Path, QID, FI), ··· 90 90 -doc false. 91 91 stat({QID, _}, Path, #{root := Root} = State) -> 92 92 FullPath = filename:join([Root] ++ Path), 93 - case file:read_file_info(FullPath, [{time, posix}]) of 93 + case file:read_file_info(FullPath, [{time, posix}, raw]) of 94 94 {ok, FileInfo} -> 95 95 Stat = file_info_to_stat(Path, QID, FileInfo), 96 96 {ok, Stat, State}; ··· 103 103 FileInfo = stat_to_file_info(Stat), 104 104 FullPath = filename:join([Root] ++ Path), 105 105 106 - case file:write_file_info(FullPath, FileInfo, [{time, posix}]) of 106 + case file:write_file_info(FullPath, FileInfo, [{time, posix}, raw]) of 107 107 ok -> {ok, State}; 108 108 {error, Reason} -> 109 109 {error, io_lib:format("Couldn't write file stat: ~p", [Reason]), ··· 115 115 FullPath = filename:join([Root] ++ Path), 116 116 QS = case e9p:is_type(QID, directory) of 117 117 true -> 118 - {ok, List} = file:list_dir(FullPath), 118 + % Currently `file` module do not expose raw mode for listing 119 + % file directory, so we need to call private `prim_file` module 120 + % to access such functionality. Otherwise we can encounter 121 + % deadlock. 122 + % 123 + % See: https://github.com/erlang/otp/issues/10593 124 + {ok, List} = prim_file:list_dir(FullPath), 119 125 {dir, List}; 120 126 false -> 121 127 {Trunc, Opts} = translate_mode(Mode), ··· 141 147 FullPath = filename:join([Root] ++ Path), 142 148 {ok, State} = clunk(FID, State0), 143 149 case case e9p:is_type(QID, directory) of 144 - true -> file:del_dir(FullPath); 145 - false -> file:delete(FullPath) 150 + % Currently `file` module do not expose raw mode for listing 151 + % file directory, so we need to call private `prim_file` module 152 + % to access such functionality. Otherwise we can encounter 153 + % deadlock. 154 + % 155 + % See: https://github.com/erlang/otp/issues/10593 156 + true -> prim_file:del_dir(FullPath); 157 + false -> file:delete(FullPath, [raw]) 146 158 end of 147 159 ok -> {ok, State}; 148 160 {error, Reason} ->
-17
src/e9p_utils.erl
··· 1 - % SPDX-FileCopyrightText: 2025 Łukasz Niemier <~@hauleth.dev> 2 - % 3 - % SPDX-License-Identifier: Apache-2.0 4 - 5 - -module(e9p_utils). 6 - 7 - -export([normalize_path/1]). 8 - 9 - normalize_path(List) -> normalize_path(List, []). 10 - 11 - normalize_path([], Acc) -> lists:reverse(Acc); 12 - normalize_path([Dot | Rest], Acc) 13 - when Dot =:= "." orelse Dot =:= ~"." 14 - -> 15 - normalize_path(Rest, Acc); 16 - normalize_path([P | Rest], Acc) -> 17 - normalize_path(Rest, [P | Acc]).
-1
test/e9p_sysfs_SUITE.erl
··· 26 26 ok = file:make_dir(Path), 27 27 {ok, PID} = e9p_server:start(Port, {e9p_sysfs, []}), 28 28 ct:pal(Path), 29 - ct:sleep(1000), 30 29 Cmd = io_lib:format("9pfs -p ~B localhost ~s", 31 30 [Port, Path]), 32 31 ct:pal(Cmd),
+47
test/e9p_unfs_SUITE.erl
··· 1 + -module(e9p_unfs_SUITE). 2 + 3 + -compile(export_all). 4 + 5 + -include_lib("stdlib/include/assert.hrl"). 6 + -include_lib("common_test/include/ct.hrl"). 7 + 8 + all() -> [ 9 + can_list_mount_content 10 + ]. 11 + 12 + init_per_suite(Config) -> 13 + PrivDir = ?config(priv_dir, Config), 14 + Source = filename:join(PrivDir, "h"), 15 + Mount = filename:join(PrivDir, "c"), 16 + Port = 6666, 17 + ok = file:make_dir(Source), 18 + ok = file:make_dir(Mount), 19 + {ok, PID} = e9p_server:start(Port, {e9p_unfs, #{path => Source}}), 20 + ct:pal("source: ~s", [Source]), 21 + ct:pal("mount: ~s", [Mount]), 22 + Cmd = io_lib:format("9pfs -p ~B localhost ~p", 23 + [Port, Mount]), 24 + ct:pal(Cmd), 25 + _Out = os:cmd(Cmd, #{ exception_on_failure => true }), 26 + [{fs, PID}, {mount, Mount}, {source, Source} | Config]. 27 + 28 + end_per_suite(Config) -> 29 + PID = ?config(fs, Config), 30 + Mount = ?config(mount, Config), 31 + os:cmd(["umount ", Mount], #{exception_on_failure => true}), 32 + erlang:exit(PID, normal), 33 + Config. 34 + 35 + can_list_mount_content(Config) -> 36 + Source = ?config(source, Config), 37 + Mount = ?config(mount, Config), 38 + file:write_file([Source, "/bar"], "example data"), 39 + ct:pal(Mount), 40 + ?assertEqual(["bar"], ls(Mount)). 41 + 42 + %% Helpers 43 + 44 + ls(Path) -> 45 + {ok, Files} = file:list_dir(Path), 46 + % Remove `.fscache` added on macOS 47 + lists:sort(Files) -- [".fscache"].
+1 -4
test/prop_e9p_msg.erl
··· 31 31 qid() -> 32 32 ?LET({Type, Version, Path}, {qid_type(), int(4), int(8)}, 33 33 e9p:make_qid(Type, Version, Path)). 34 - qid(Type) -> 35 - ?LET({Version, Path}, {int(4), int(8)}, 36 - e9p:make_qid(Type, Version, Path)). 37 34 38 35 prop_tversion() -> 39 36 ?FORALL({Version, MPS}, {bin_str(), int(4)}, ··· 120 117 enc_dec(#tstat{fid = FID})). 121 118 prop_rstat() -> 122 119 ?FORALL({QID, Type, Dev, Mode, Atime, Mtime, Len, Name, Uid, Gid, Muid}, 123 - {qid(), int(2), int(2), int(4), int(4), int(4), int(8), bin_str(), bin_str(), 120 + {qid(), int(2), int(2), integer(0, 8#777), int(4), int(4), int(8), bin_str(), bin_str(), 124 121 bin_str(), bin_str()}, 125 122 begin 126 123 enc_dec(#rstat{