@@ -172,6 +172,117 @@ mod server {
172172 }
173173 }
174174
175+ /// Windows pipe security: grant Everyone read/write and stamp a Low
176+ /// mandatory integrity label so a Medium-IL `fluxdown_nmh.exe` (spawned by
177+ /// the browser) can connect even when FluxDown runs elevated (High IL).
178+ /// Without it the pipe inherits the creator's High IL and the no-write-up
179+ /// rule silently rejects the relay — browser interception dies until the
180+ /// app next runs unelevated.
181+ mod pipe_security {
182+ use std:: ffi:: c_void;
183+ use std:: io;
184+
185+ #[ repr( C ) ]
186+ struct SecurityAttributes {
187+ n_length : u32 ,
188+ lp_security_descriptor : * mut c_void ,
189+ b_inherit_handle : i32 ,
190+ }
191+
192+ #[ link( name = "advapi32" ) ]
193+ unsafe extern "system" {
194+ fn ConvertStringSecurityDescriptorToSecurityDescriptorW (
195+ string_security_descriptor : * const u16 ,
196+ string_sddl_revision : u32 ,
197+ security_descriptor : * mut * mut c_void ,
198+ security_descriptor_size : * mut u32 ,
199+ ) -> i32 ;
200+ }
201+
202+ #[ link( name = "kernel32" ) ]
203+ unsafe extern "system" {
204+ fn LocalFree ( hmem : * mut c_void ) -> * mut c_void ;
205+ }
206+
207+ /// Owns the security descriptor allocated by the SDDL conversion plus
208+ /// the `SECURITY_ATTRIBUTES` pointing at it; frees it on drop. Never
209+ /// held across an `.await` (built and dropped inside the synchronous
210+ /// `create_instance`), so the raw pointer needs no `Send`.
211+ pub struct PipeSecurity {
212+ attrs : SecurityAttributes ,
213+ }
214+
215+ impl PipeSecurity {
216+ /// Build the descriptor, or error if the SDDL conversion fails.
217+ pub fn new ( ) -> io:: Result < Self > {
218+ // D: Everyone (WD) generic read+write. S: Low mandatory label,
219+ // no-write-up (equal/higher IL subjects may still write).
220+ let sddl: Vec < u16 > = "D:(A;;GRGW;;;WD)S:(ML;;NW;;;LW)"
221+ . encode_utf16 ( )
222+ . chain ( std:: iter:: once ( 0 ) )
223+ . collect ( ) ;
224+ let mut psd: * mut c_void = std:: ptr:: null_mut ( ) ;
225+ // SAFETY: `sddl` is a valid NUL-terminated UTF-16 string; on
226+ // success the call allocates `psd`, freed in `Drop`. Revision
227+ // 1 == SDDL_REVISION_1.
228+ let ok = unsafe {
229+ ConvertStringSecurityDescriptorToSecurityDescriptorW (
230+ sddl. as_ptr ( ) ,
231+ 1 ,
232+ & mut psd,
233+ std:: ptr:: null_mut ( ) ,
234+ )
235+ } ;
236+ if ok == 0 {
237+ return Err ( io:: Error :: last_os_error ( ) ) ;
238+ }
239+ Ok ( Self {
240+ attrs : SecurityAttributes {
241+ n_length : std:: mem:: size_of :: < SecurityAttributes > ( ) as u32 ,
242+ lp_security_descriptor : psd,
243+ b_inherit_handle : 0 ,
244+ } ,
245+ } )
246+ }
247+
248+ /// Pointer to the `SECURITY_ATTRIBUTES`, valid while `self` lives.
249+ pub fn as_ptr ( & self ) -> * mut c_void {
250+ ( & raw const self . attrs ) . cast :: < c_void > ( ) . cast_mut ( )
251+ }
252+ }
253+
254+ impl Drop for PipeSecurity {
255+ fn drop ( & mut self ) {
256+ if !self . attrs . lp_security_descriptor . is_null ( ) {
257+ // SAFETY: descriptor was allocated by the SDDL conversion.
258+ unsafe { LocalFree ( self . attrs . lp_security_descriptor ) } ;
259+ }
260+ }
261+ }
262+ }
263+
264+ /// Create one pipe server instance with a hardened security descriptor
265+ /// (Everyone R/W + Low integrity label) so a Medium-IL NMH relay can
266+ /// connect even when FluxDown runs elevated. Falls back to default security
267+ /// if the descriptor cannot be built, never breaking the unelevated path.
268+ fn create_instance (
269+ first : bool ,
270+ ) -> std:: io:: Result < tokio:: net:: windows:: named_pipe:: NamedPipeServer > {
271+ let mut options = ServerOptions :: new ( ) ;
272+ options. first_pipe_instance ( first) ;
273+ match pipe_security:: PipeSecurity :: new ( ) {
274+ // SAFETY: `sec` and its descriptor outlive this create call, which
275+ // copies the SECURITY_ATTRIBUTES into the pipe synchronously.
276+ Ok ( sec) => unsafe {
277+ options. create_with_security_attributes_raw ( PIPE_NAME , sec. as_ptr ( ) )
278+ } ,
279+ Err ( e) => {
280+ log_info ! ( "[nmh-pipe] pipe security unavailable, using default: {}" , e) ;
281+ options. create ( PIPE_NAME )
282+ }
283+ }
284+ }
285+
175286 /// Spawn the Named Pipe server, feeding download requests into `tx`.
176287 /// The receiving end is owned by `download_actor` and shared with the
177288 /// local HTTP takeover server so both transports converge on one channel.
@@ -180,10 +291,7 @@ mod server {
180291 log_info ! ( "[nmh-pipe] starting Named Pipe server at {}" , PIPE_NAME ) ;
181292
182293 // Create the first server instance before entering the loop.
183- let mut server = match ServerOptions :: new ( )
184- . first_pipe_instance ( true )
185- . create ( PIPE_NAME )
186- {
294+ let mut server = match create_instance ( true ) {
187295 Ok ( s) => s,
188296 Err ( e) => {
189297 log_info ! ( "[nmh-pipe] failed to create pipe server: {}" , e) ;
@@ -204,7 +312,7 @@ mod server {
204312
205313 // Create the next server instance to accept the next client
206314 // while we handle the current one.
207- let next_server = match ServerOptions :: new ( ) . create ( PIPE_NAME ) {
315+ let next_server = match create_instance ( false ) {
208316 Ok ( s) => s,
209317 Err ( e) => {
210318 log_info ! ( "[nmh-pipe] failed to create next pipe instance: {}" , e) ;
0 commit comments