From 8e41214004a9fb16d9eac41c34c2d559a838ce7b Mon Sep 17 00:00:00 2001 From: Jordan Milne Date: Mon, 13 Nov 2023 00:53:23 +0000 Subject: [PATCH] Attempt to support mounting with default arguments under new macOS This was caused by a change in macOS that causes the mount to block looking for the statd program, unless explicitly mounting without locks. We handle this by providing a stub statd that always returns fail. --- compile_idl.sh | 1 + shenaniganfs/generated/statd.py | 158 ++++++++++++++++++++++++++++++++ shenaniganfs/idl/statd.x | 86 +++++++++++++++++ shenaniganfs/nfs_utils.py | 2 + shenaniganfs/statd.py | 36 ++++++++ shenaniganfs/tools/rpcgen.py | 10 +- 6 files changed, 291 insertions(+), 2 deletions(-) create mode 100644 shenaniganfs/generated/statd.py create mode 100644 shenaniganfs/idl/statd.x create mode 100644 shenaniganfs/statd.py diff --git a/compile_idl.sh b/compile_idl.sh index 89bc845..268ada4 100755 --- a/compile_idl.sh +++ b/compile_idl.sh @@ -12,5 +12,6 @@ compile_idl rfc1813 compile_idl rfc1831 compile_idl rfc1833_portmapper compile_idl rfc1833_rpcbind +compile_idl statd echo "Done" diff --git a/shenaniganfs/generated/statd.py b/shenaniganfs/generated/statd.py new file mode 100644 index 0000000..6648a36 --- /dev/null +++ b/shenaniganfs/generated/statd.py @@ -0,0 +1,158 @@ +# Auto-generated from IDL file +import abc +import dataclasses +import typing +from dataclasses import dataclass + +from shenaniganfs import rpchelp + +TRUE = True +FALSE = False +stat_succ = 0 +stat_fail = 1 +SM_MAXSTRLEN = 1024 +SM_PRIV_SIZE = 16 + + +@dataclass +class SmName(rpchelp.Struct): # sm_name + mon_name: bytes = rpchelp.rpc_field(rpchelp.Opaque(rpchelp.LengthType.VAR, SM_MAXSTRLEN)) + + +@dataclass +class MyId(rpchelp.Struct): # my_id + my_name: bytes = rpchelp.rpc_field(rpchelp.Opaque(rpchelp.LengthType.VAR, SM_MAXSTRLEN)) + my_prog: int = rpchelp.rpc_field(rpchelp.r_int) + my_vers: int = rpchelp.rpc_field(rpchelp.r_int) + my_proc: int = rpchelp.rpc_field(rpchelp.r_int) + + +@dataclass +class MonId(rpchelp.Struct): # mon_id + mon_name: bytes = rpchelp.rpc_field(rpchelp.Opaque(rpchelp.LengthType.VAR, SM_MAXSTRLEN)) + my_id: MyId = rpchelp.rpc_field(MyId) + + +@dataclass +class Mon(rpchelp.Struct): # mon + mon_id: MonId = rpchelp.rpc_field(MonId) + priv: bytes = rpchelp.rpc_field(rpchelp.Opaque(rpchelp.LengthType.FIXED, SM_PRIV_SIZE)) + + +@dataclass +class StatChge(rpchelp.Struct): # stat_chge + mon_name: bytes = rpchelp.rpc_field(rpchelp.Opaque(rpchelp.LengthType.VAR, SM_MAXSTRLEN)) + state: int = rpchelp.rpc_field(rpchelp.r_int) + + +@dataclass +class SmStat(rpchelp.Struct): # sm_stat + state: int = rpchelp.rpc_field(rpchelp.r_int) + + +class Res(rpchelp.Enum): # res + stat_succ = 0 + stat_fail = 1 + + +@dataclass +class SmStatRes(rpchelp.Struct): # sm_stat_res + res_stat: typing.Union[Res, int] = rpchelp.rpc_field(Res) + state: int = rpchelp.rpc_field(rpchelp.r_int) + + +@dataclass +class Status(rpchelp.Struct): # status + mon_name: bytes = rpchelp.rpc_field(rpchelp.Opaque(rpchelp.LengthType.VAR, SM_MAXSTRLEN)) + state: int = rpchelp.rpc_field(rpchelp.r_int) + priv: bytes = rpchelp.rpc_field(rpchelp.Opaque(rpchelp.LengthType.FIXED, SM_PRIV_SIZE)) + + +from shenaniganfs import client, transport + + +class SM_PROG_1_SERVER(transport.Prog): + prog = 100024 + vers = 1 + procs = { + 0: rpchelp.Proc('NULL', rpchelp.r_void, []), + 1: rpchelp.Proc('STAT', SmStatRes, [SmName]), + 2: rpchelp.Proc('MON', SmStatRes, [Mon]), + 3: rpchelp.Proc('UNMON', SmStat, [MonId]), + 4: rpchelp.Proc('UNMON_ALL', SmStat, [MyId]), + 5: rpchelp.Proc('SIMU_CRASH', rpchelp.r_void, []), + 6: rpchelp.Proc('NOTIFY', rpchelp.r_void, [StatChge]), + } + + @abc.abstractmethod + async def NULL(self, call_ctx: transport.CallContext) \ + -> transport.ProcRet[None]: + raise NotImplementedError() + + @abc.abstractmethod + async def STAT(self, call_ctx: transport.CallContext, arg_0: SmName) \ + -> transport.ProcRet[SmStatRes]: + raise NotImplementedError() + + @abc.abstractmethod + async def MON(self, call_ctx: transport.CallContext, arg_0: Mon) \ + -> transport.ProcRet[SmStatRes]: + raise NotImplementedError() + + @abc.abstractmethod + async def UNMON(self, call_ctx: transport.CallContext, arg_0: MonId) \ + -> transport.ProcRet[SmStat]: + raise NotImplementedError() + + @abc.abstractmethod + async def UNMON_ALL(self, call_ctx: transport.CallContext, arg_0: MyId) \ + -> transport.ProcRet[SmStat]: + raise NotImplementedError() + + @abc.abstractmethod + async def SIMU_CRASH(self, call_ctx: transport.CallContext) \ + -> transport.ProcRet[None]: + raise NotImplementedError() + + @abc.abstractmethod + async def NOTIFY(self, call_ctx: transport.CallContext, arg_0: StatChge) \ + -> transport.ProcRet[None]: + raise NotImplementedError() + + +class SM_PROG_1_CLIENT(client.BaseClient): + prog = 100024 + vers = 1 + procs = { + 0: rpchelp.Proc('NULL', rpchelp.r_void, []), + 1: rpchelp.Proc('STAT', SmStatRes, [SmName]), + 2: rpchelp.Proc('MON', SmStatRes, [Mon]), + 3: rpchelp.Proc('UNMON', SmStat, [MonId]), + 4: rpchelp.Proc('UNMON_ALL', SmStat, [MyId]), + 5: rpchelp.Proc('SIMU_CRASH', rpchelp.r_void, []), + 6: rpchelp.Proc('NOTIFY', rpchelp.r_void, [StatChge]), + } + + async def NULL(self) -> client.UnpackedRPCMsg[None]: + return await self.send_call(0, ) + + async def STAT(self, arg_0: SmName) -> client.UnpackedRPCMsg[SmStatRes]: + return await self.send_call(1, arg_0) + + async def MON(self, arg_0: Mon) -> client.UnpackedRPCMsg[SmStatRes]: + return await self.send_call(2, arg_0) + + async def UNMON(self, arg_0: MonId) -> client.UnpackedRPCMsg[SmStat]: + return await self.send_call(3, arg_0) + + async def UNMON_ALL(self, arg_0: MyId) -> client.UnpackedRPCMsg[SmStat]: + return await self.send_call(4, arg_0) + + async def SIMU_CRASH(self) -> client.UnpackedRPCMsg[None]: + return await self.send_call(5, ) + + async def NOTIFY(self, arg_0: StatChge) -> client.UnpackedRPCMsg[None]: + return await self.send_call(6, arg_0) + + +__all__ = ['SM_PROG_1_SERVER', 'SM_PROG_1_CLIENT', 'TRUE', 'FALSE', 'SM_MAXSTRLEN', 'SM_PRIV_SIZE', 'stat_succ', 'stat_fail', 'SmName', 'MyId', 'MonId', 'Mon', 'StatChge', 'SmStat', 'Res', 'SmStatRes', 'Status'] diff --git a/shenaniganfs/idl/statd.x b/shenaniganfs/idl/statd.x new file mode 100644 index 0000000..dfddd07 --- /dev/null +++ b/shenaniganfs/idl/statd.x @@ -0,0 +1,86 @@ + +const SM_MAXSTRLEN = 1024; +const SM_PRIV_SIZE = 16; + +struct sm_name { + string mon_name; +}; + +struct my_id { + string my_name; /* name of the site iniates the monitoring request*/ + int my_prog; /* rpc program # of the requesting process */ + int my_vers; /* rpc version # of the requesting process */ + int my_proc; /* rpc procedure # of the requesting process */ +}; + +struct mon_id { + string mon_name; /* name of the site to be monitored */ + my_id my_id; +}; + + +struct mon { + mon_id mon_id; + opaque priv[SM_PRIV_SIZE]; /* private information to store at monitor for requesting process */ +}; + +struct stat_chge { + string mon_name; /* name of the site that had the state change */ + int state; +}; + +/* + * state # of status monitor monitonically increases each time + * status of the site changes: + * an even number (>= 0) indicates the site is down and + * an odd number (> 0) indicates the site is up; + */ +struct sm_stat { + int state; /* state # of status monitor */ +}; + +enum res { + stat_succ = 0, /* status monitor agrees to monitor */ + stat_fail = 1 /* status monitor cannot monitor */ +}; + +struct sm_stat_res { + res res_stat; + int state; +}; + +/* + * structure of the status message sent back by the status monitor + * when monitor site status changes + */ +struct status { + string mon_name; + int state; + opaque priv[SM_PRIV_SIZE]; /* stored private information */ +}; + + +program SM_PROG { + version SM_VERS { + /* res_stat = stat_succ if status monitor agrees to monitor */ + /* res_stat = stat_fail if status monitor cannot monitor */ + /* if res_stat == stat_succ, state = state number of site sm_name */ + sm_stat_res SM_STAT(sm_name) = 1; + + /* res_stat = stat_succ if status monitor agrees to monitor */ + /* res_stat = stat_fail if status monitor cannot monitor */ + /* stat consists of state number of local site */ + sm_stat_res SM_MON(mon) = 2; + + /* stat consists of state number of local site */ + sm_stat SM_UNMON(mon_id) = 3; + + /* stat consists of state number of local site */ + sm_stat SM_UNMON_ALL(my_id) = 4; + + void SM_SIMU_CRASH(void) = 5; + + void SM_NOTIFY(stat_chge) = 6; + + } = 1; +} = 100024; diff --git a/shenaniganfs/nfs_utils.py b/shenaniganfs/nfs_utils.py index c1fe5ff..0d28207 100644 --- a/shenaniganfs/nfs_utils.py +++ b/shenaniganfs/nfs_utils.py @@ -3,6 +3,7 @@ from shenaniganfs.nfs3 import MountV3Service, NFSV3Service from shenaniganfs.portmanager import PortManager, SimplePortMapper, SimpleRPCBind from shenaniganfs.server import TCPTransportServer +from shenaniganfs.statd import StatDV1Server async def serve_nfs(fs_manager: FileSystemManager, use_internal_rpcbind=True): @@ -20,6 +21,7 @@ async def serve_nfs(fs_manager: FileSystemManager, use_internal_rpcbind=True): transport_server.register_prog(NFSV2Service(fs_manager)) transport_server.register_prog(MountV3Service(fs_manager)) transport_server.register_prog(NFSV3Service(fs_manager)) + transport_server.register_prog(StatDV1Server()) if use_internal_rpcbind: transport_server.notify_port_manager(port_manager) else: diff --git a/shenaniganfs/statd.py b/shenaniganfs/statd.py new file mode 100644 index 0000000..5574c70 --- /dev/null +++ b/shenaniganfs/statd.py @@ -0,0 +1,36 @@ +""" +A statd stub so that new versions of macOS can connect without extra options, +see https://github.com/apple/darwin-xnu/blob/2ff845c2e033bd0ff64b5b6aa6063a1f8f65aa32/bsd/nfs/nfs_vfsops.c#L4622-L4655 + +Previously mount on macOS was hanging unless an option was passed to mount without locks. +""" + +from shenaniganfs import transport +from shenaniganfs.generated.statd import * + + +class StatDV1Server(SM_PROG_1_SERVER): + async def NULL(self, call_ctx: transport.CallContext) -> transport.ProcRet[None]: + pass + + async def STAT(self, call_ctx: transport.CallContext, arg_0: SmName) -> transport.ProcRet[SmStatRes]: + # Just say fail no matter what + return SmStatRes( + res_stat=Res.stat_fail, + state=0, + ) + + async def MON(self, call_ctx: transport.CallContext, arg_0: Mon) -> transport.ProcRet[SmStatRes]: + raise NotImplementedError() + + async def UNMON(self, call_ctx: transport.CallContext, arg_0: MonId) -> transport.ProcRet[SmStat]: + raise NotImplementedError() + + async def UNMON_ALL(self, call_ctx: transport.CallContext, arg_0: MyId) -> transport.ProcRet[SmStat]: + raise NotImplementedError() + + async def SIMU_CRASH(self, call_ctx: transport.CallContext) -> transport.ProcRet[None]: + raise NotImplementedError() + + async def NOTIFY(self, call_ctx: transport.CallContext, arg_0: StatChge) -> transport.ProcRet[None]: + raise NotImplementedError() diff --git a/shenaniganfs/tools/rpcgen.py b/shenaniganfs/tools/rpcgen.py index 5ae1f62..5b79399 100644 --- a/shenaniganfs/tools/rpcgen.py +++ b/shenaniganfs/tools/rpcgen.py @@ -558,9 +558,15 @@ def _common_prefix(vals): prefix = prefix[:last_match] return prefix - def remove_common_prefix(self): + def remove_common_prefix(self, vers_ident: typing.Optional[str] = None): if len(self.children) == 1: + # Only one child, can't check if there's a common prefix across children, + # just check if there's a version ident prefix instead + child = self.children[0] + if vers_ident and child.ident.startswith(vers_ident) and vers_ident != child.ident: + child.ident = child.ident[len(vers_ident):].lstrip("_") return + prefix = self._common_prefix([p.ident for p in self.children]) if not prefix: return @@ -677,7 +683,7 @@ class Version(Node): def __init__(self, ident, proc_defs, version_id): Node.__init__(self) self.ident = ident - proc_defs.remove_common_prefix() + proc_defs.remove_common_prefix(ident) # All programs have an implicit NULL procedure if not any(str(proc.proc_id) == "0" for proc in proc_defs.children): void_spec = TypeSpec("void", False, True, False)