E-MailRelay
gprocess_unix.cpp
Go to the documentation of this file.
1//
2// Copyright (C) 2001-2023 Graeme Walker <graeme_walker@users.sourceforge.net>
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program. If not, see <http://www.gnu.org/licenses/>.
16// ===
17///
18/// \file gprocess_unix.cpp
19///
20
21#include "gdef.h"
22#include "gprocess.h"
23#include "gidentity.h"
24#include "gstr.h"
25#include "gfile.h"
26#include "gpath.h"
27#include "glimits.h"
28#include "glog.h"
29#include <iostream>
30#include <stdexcept>
31#include <array>
32#include <cstring> // std::strerror()
33#include <climits> // PATH_MAX
34#include <cerrno> // errno
35#include <fcntl.h>
36
37namespace G
38{
39 namespace ProcessImp
40 {
41 G_EXCEPTION_CLASS( DevNullError , tx("cannot open /dev/null") ) ;
42 G_EXCEPTION_CLASS( IdentityError , tx("cannot change process identity") ) ;
43 void noCloseOnExec( int fd ) noexcept ;
44 void reopen( int fd , int mode ) ;
45 mode_t umaskValue( G::Process::Umask::Mode mode ) ;
46 bool readlink_( string_view path , std::string & value ) ;
47 bool setRealUser( Identity id , std::nothrow_t ) noexcept ;
48 bool setRealGroup( Identity id , std::nothrow_t ) noexcept ;
49 void setEffectiveUser( Identity id ) ;
50 bool setEffectiveUser( Identity id , std::nothrow_t ) noexcept ;
51 void setEffectiveGroup( Identity id ) ;
52 bool setEffectiveGroup( Identity id , std::nothrow_t ) noexcept ;
53 void throwError() ;
54 void beSpecial( Identity special_identity , bool change_group ) ;
55 void beSpecialForExit( SignalSafe , Identity special_identity ) noexcept ;
56 Identity beOrdinaryAtStartup( Identity , bool change_group ) ;
57 Identity beOrdinary( Identity , bool change_group ) ;
58 void beOrdinaryForExec( Identity run_as_id ) noexcept ;
59 void revokeExtraGroups() ;
60 }
61}
62
63class G::Process::UmaskImp
64{
65public:
66 mode_t m_old_mode ;
67 static mode_t set( Process::Umask::Mode ) noexcept ;
68 static void set( mode_t ) noexcept ;
69} ;
70
71// ==
72
73void G::Process::cd( const Path & dir )
74{
75 if( ! cd(dir,std::nothrow) )
76 throw CannotChangeDirectory( dir.str() ) ;
77}
78
79bool G::Process::cd( const Path & dir , std::nothrow_t )
80{
81 return 0 == ::chdir( dir.cstr() ) ;
82}
83
85{
86 ProcessImp::reopen( STDERR_FILENO , O_WRONLY ) ;
87}
88
89void G::Process::closeFiles( bool keep_stderr )
90{
91 std::cout << std::flush ;
92 std::cerr << std::flush ;
93
94 ProcessImp::reopen( STDIN_FILENO , O_RDONLY ) ;
95 ProcessImp::reopen( STDOUT_FILENO , O_WRONLY ) ;
96 if( !keep_stderr )
97 ProcessImp::reopen( STDERR_FILENO , O_WRONLY ) ;
98
99 closeOtherFiles() ;
100}
101
103{
104 int n = 256U ;
105 long rc = ::sysconf( _SC_OPEN_MAX ) ;
106 if( rc > 0L )
107 n = static_cast<int>( rc ) ;
108
109 for( int fd = 0 ; fd < n ; fd++ )
110 {
111 if( fd != STDIN_FILENO && fd != STDOUT_FILENO && fd != STDERR_FILENO && fd != fd_keep )
112 ::close( fd ) ;
113 }
114 ProcessImp::noCloseOnExec( STDIN_FILENO ) ;
115 ProcessImp::noCloseOnExec( STDOUT_FILENO ) ;
116 ProcessImp::noCloseOnExec( STDERR_FILENO ) ;
117}
118
119int G::Process::errno_( const SignalSafe & ) noexcept
120{
121 return errno ; // possible macro, not ::errno or std::errno
122}
123
124void G::Process::errno_( int e_new ) noexcept
125{
126 errno = e_new ;
127}
128
129int G::Process::errno_( const SignalSafe & , int e_new ) noexcept
130{
131 int e_old = errno ;
132 errno = e_new ;
133 return e_old ;
134}
135
136std::string G::Process::strerror( int errno_ )
137{
138 char * p = std::strerror( errno_ ) ;
139 std::string s( p ? p : "" ) ;
140 if( s.empty() ) s = "unknown error" ;
141 return Str::isPrintableAscii(s) ? Str::lower(s) : s ;
142}
143
144void G::Process::beSpecial( Identity special_identity , bool change_group )
145{
146 ProcessImp::beSpecial( special_identity , change_group ) ;
147}
148
149void G::Process::beSpecialForExit( SignalSafe , Identity special_identity ) noexcept
150{
151 ProcessImp::beSpecialForExit( SignalSafe() , special_identity ) ;
152}
153
154std::pair<G::Identity,G::Identity> G::Process::beOrdinaryAtStartup( const std::string & ordinary_name ,
155 bool change_group )
156{
157 Identity ordinary_id( ordinary_name ) ;
158
159 // revoke extra groups, but not if we are leaving groups alone
160 // or we have been given root as the non-root user or we
161 // are running vanilla
162 if( change_group && !ordinary_id.isRoot() )
163 {
164 Identity real = Identity::real() ;
165 if( real.isRoot() || real != Identity::effective() )
166 ProcessImp::revokeExtraGroups() ;
167 }
168
169 Identity startup_id = ProcessImp::beOrdinary( ordinary_id , change_group ) ;
170 return { ordinary_id , startup_id } ;
171}
172
173void G::Process::beOrdinary( Identity ordinary_id , bool change_group )
174{
175 ProcessImp::beOrdinary( ordinary_id , change_group ) ;
176}
177
178void G::Process::beOrdinaryForExec( Identity run_as_id ) noexcept
179{
180 ProcessImp::beOrdinaryForExec( run_as_id ) ;
181}
182
183#ifndef G_LIB_SMALL
185{
186 G::ProcessImp::setEffectiveUser( id ) ;
187}
188#endif
189
190#ifndef G_LIB_SMALL
192{
193 G::ProcessImp::setEffectiveGroup( id ) ;
194}
195#endif
196
197std::string G::Process::cwd( bool no_throw )
198{
199 std::string result ;
200 std::array<std::size_t,2U> sizes = {{ G::Limits<>::path_buffer , PATH_MAX+1U }} ;
201 for( std::size_t n : sizes )
202 {
203 std::vector<char> buffer( n ) ;
204 char * p = getcwd( &buffer[0] , buffer.size() ) ;
205 int error = errno_() ;
206 if( p != nullptr )
207 {
208 buffer.push_back( '\0' ) ;
209 result.assign( &buffer[0] ) ;
210 break ;
211 }
212 else if( error != ERANGE )
213 {
214 break ;
215 }
216 }
217 if( result.empty() && !no_throw )
218 throw GetCwdError() ;
219 return result ;
220}
221
222#ifdef G_UNIX_MAC
223#include <libproc.h>
224std::string G::Process::exe()
225{
226 // (see also _NSGetExecutablePath())
227 std::vector<char> buffer( std::max(100,PROC_PIDPATHINFO_MAXSIZE) ) ;
228 buffer[0] = '\0' ;
229 int rc = proc_pidpath( getpid() , &buffer[0] , buffer.size() ) ;
230 if( rc > 0 )
231 {
232 std::size_t n = static_cast<std::size_t>(rc) ;
233 if( n > buffer.size() ) n = buffer.size() ;
234 return std::string( &buffer[0] , n ) ;
235 }
236 else
237 {
238 return std::string() ;
239 }
240}
241#else
242std::string G::Process::exe()
243{
244 // best effort, not guaranteed
245 std::string result ;
246 ProcessImp::readlink_( "/proc/self/exe" , result ) ||
247 ProcessImp::readlink_( "/proc/curproc/file" , result ) ||
248 ProcessImp::readlink_( "/proc/curproc/exe" , result ) ;
249 return result ;
250}
251#endif
252
253// ==
254
255G::Process::Id::Id() noexcept :
256 m_pid(::getpid())
257{
258}
259
260std::string G::Process::Id::str() const
261{
262 std::ostringstream ss ;
263 ss << m_pid ;
264 return ss.str() ;
265}
266
267#ifndef G_LIB_SMALL
268bool G::Process::Id::operator==( const Id & other ) const noexcept
269{
270 return m_pid == other.m_pid ;
271}
272#endif
273
274bool G::Process::Id::operator!=( const Id & other ) const noexcept
275{
276 return m_pid != other.m_pid ;
277}
278
279// ==
280
281void G::Process::UmaskImp::set( mode_t mode ) noexcept
282{
283 GDEF_IGNORE_RETURN ::umask( mode ) ;
284}
285
286mode_t G::Process::UmaskImp::set( Umask::Mode mode ) noexcept
287{
288 mode_t old = ::umask( 2 ) ;
289 mode_t new_ = old ;
290 if( mode == Umask::Mode::Tightest )
291 new_ = 0077 ; // -rw-------
292 else if( mode == Umask::Mode::Tighter )
293 new_ = 0007 ; // -rw-rw----
294 else if( mode == Umask::Mode::Readable )
295 new_ = 0022 ; // -rw-r--r--
296 else if( mode == Umask::Mode::GroupOpen )
297 new_ = 0002 ; // -rw-rw-r--
298 else if( mode == Umask::Mode::TightenOther )
299 new_ = old | mode_t(007) ;
300 else if( mode == Umask::Mode::LoosenGroup )
301 new_ = old & ~mode_t(070) ;
302 else if( mode == Umask::Mode::Open )
303 new_ = 0 ; // -rw-rw-rw-
304 set( new_ ) ;
305 return old ;
306}
307
308G::Process::Umask::Umask( Mode mode ) :
309 m_imp(std::make_unique<UmaskImp>())
310{
311 m_imp->m_old_mode = UmaskImp::set( mode ) ;
312}
313
314G::Process::Umask::~Umask()
315{
316 UmaskImp::set( m_imp->m_old_mode ) ;
317}
318
319void G::Process::Umask::set( Mode mode )
320{
321 UmaskImp::set( mode ) ;
322}
323
324#ifndef G_LIB_SMALL
325void G::Process::Umask::tightenOther()
326{
327 set( Mode::TightenOther ) ;
328}
329#endif
330
331#ifndef G_LIB_SMALL
332void G::Process::Umask::loosenGroup()
333{
334 set( Mode::LoosenGroup ) ;
335}
336#endif
337
338// ==
339
340void G::ProcessImp::noCloseOnExec( int fd ) noexcept
341{
342 ::fcntl( fd , F_SETFD , 0 ) ;
343}
344
345void G::ProcessImp::reopen( int fd , int mode )
346{
347 int fd_null = ::open( Path::nullDevice().cstr() , mode ) ; // NOLINT
348 if( fd_null < 0 ) throw DevNullError() ;
349 ::dup2( fd_null , fd ) ;
350 ::close( fd_null ) ;
351}
352
353#ifndef G_LIB_SMALL
354mode_t G::ProcessImp::umaskValue( Process::Umask::Mode mode )
355{
356 mode_t m = 0 ;
357 if( mode == Process::Umask::Mode::Tightest ) m = 0177 ; // -rw-------
358 if( mode == Process::Umask::Mode::Tighter ) m = 0117 ; // -rw-rw----
359 if( mode == Process::Umask::Mode::Readable ) m = 0133 ; // -rw-r--r--
360 if( mode == Process::Umask::Mode::GroupOpen ) m = 0113 ;// -rw-rw-r--
361 return m ;
362}
363#endif
364
365bool G::ProcessImp::readlink_( string_view path , std::string & value )
366{
367 Path target = File::readlink( Path(path) , std::nothrow ) ;
368 if( !target.empty() ) value = target.str() ;
369 return !target.empty() ;
370}
371
372// ==
373
374G::Identity G::ProcessImp::beOrdinary( Identity nobody_id , bool change_group )
375{
376 Identity old_id = Identity::effective() ;
377 Identity real_id = Identity::real() ;
378 if( real_id.isRoot() )
379 {
380 if( change_group )
381 {
382 // make sure we have privilege to change group
383 if( !setEffectiveUser( Identity::root() , std::nothrow ) )
384 throwError() ;
385
386 if( !setEffectiveGroup( nobody_id , std::nothrow ) )
387 {
388 setEffectiveUser( old_id , std::nothrow ) ; // rollback
389 throwError() ;
390 }
391 }
392 if( !setEffectiveUser( nobody_id , std::nothrow ) )
393 throwError() ;
394 }
395 else
396 {
397 // change to real id -- drops suid privileges
398 if( !setEffectiveUser( real_id , std::nothrow ) )
399 throwError() ;
400
401 if( change_group && !setEffectiveGroup( real_id , std::nothrow ) )
402 {
403 setEffectiveUser( old_id , std::nothrow ) ; // rollback
404 throwError() ;
405 }
406 }
407 return old_id ;
408}
409
410void G::ProcessImp::beOrdinaryForExec( Identity run_as_id ) noexcept
411{
412 if( run_as_id != Identity::invalid() )
413 {
414 setEffectiveUser( Identity::root() , std::nothrow ) ; // for root-suid
415 setRealGroup( run_as_id , std::nothrow ) ;
416 setEffectiveGroup( run_as_id , std::nothrow ) ;
417 setRealUser( run_as_id , std::nothrow ) ;
418 setEffectiveUser( run_as_id , std::nothrow ) ;
419 }
420}
421
422void G::ProcessImp::beSpecial( Identity special_identity , bool change_group )
423{
424 setEffectiveUser( special_identity ) ;
425 if( change_group )
426 setEffectiveGroup( special_identity ) ;
427}
428
429void G::ProcessImp::beSpecialForExit( SignalSafe , Identity special_identity ) noexcept
430{
431 // changing effective ids is not strictly signal-safe :-<
432 setEffectiveUser( special_identity , std::nothrow ) ;
433 setEffectiveGroup( special_identity , std::nothrow ) ;
434}
435
436void G::ProcessImp::revokeExtraGroups()
437{
438 if( Identity::real().isRoot() || Identity::effective() != Identity::real() )
439 {
440 // set supplementary group-ids to a zero-length list
441 gid_t dummy = 0 ;
442 GDEF_IGNORE_RETURN ::setgroups( 0U , &dummy ) ; // (only works for root, so ignore the return code)
443 }
444}
445
446bool G::ProcessImp::setRealUser( Identity id , std::nothrow_t ) noexcept
447{
448 return 0 == ::setuid( id.userid() ) ;
449}
450
451void G::ProcessImp::setEffectiveUser( Identity id )
452{
453 if( ::seteuid(id.userid()) )
454 {
455 int e = errno ;
456 throw Process::UidError( Process::strerror(e) ) ;
457 }
458}
459
460bool G::ProcessImp::setEffectiveUser( Identity id , std::nothrow_t ) noexcept
461{
462 return 0 == ::seteuid( id.userid() ) ;
463}
464
465bool G::ProcessImp::setRealGroup( Identity id , std::nothrow_t ) noexcept
466{
467 return 0 == ::setgid( id.groupid() ) ;
468}
469
470void G::ProcessImp::setEffectiveGroup( Identity id )
471{
472 if( ::setegid(id.groupid()) )
473 {
474 int e = errno ;
475 throw Process::GidError( Process::strerror(e) ) ;
476 }
477}
478
479bool G::ProcessImp::setEffectiveGroup( Identity id , std::nothrow_t ) noexcept
480{
481 return 0 == ::setegid( id.groupid() ) ;
482}
483
484void G::ProcessImp::throwError()
485{
486 // typically we are about to std::terminate() so make sure there is an error message
487 G_ERROR( "G::ProcessImp::throwError: failed to change process identity" ) ;
488 throw IdentityError() ;
489}
490
static G::Path readlink(const Path &link)
Reads a symlink. Throws on error.
Definition: gfile_unix.cpp:396
A combination of user-id and group-id, with a very low-level interface to the get/set/e/uid/gid funct...
Definition: gidentity.h:44
bool isRoot() const noexcept
Returns true if the userid is zero.
static Identity invalid() noexcept
Returns an invalid identity.
static Identity root() noexcept
Returns the superuser identity.
static Identity effective() noexcept
Returns the current effective identity.
static Identity real() noexcept
Returns the calling process's real identity.
A Path object represents a file system path.
Definition: gpath.h:73
const char * cstr() const noexcept
Returns the path string.
Definition: gpath.h:230
static Path nullDevice()
Returns the path of the "/dev/null" special file, or equivalent.
Definition: gpath.cpp:326
std::string str() const
Returns the path string.
Definition: gpath.h:224
static void beSpecialForExit(SignalSafe, Identity special_id) noexcept
A signal-safe version of beSpecial() that should only be used just before process exit.
static void closeOtherFiles(int fd_keep=-1)
Closes all open file descriptors except the three standard ones and possibly one other.
static std::pair< Identity, Identity > beOrdinaryAtStartup(const std::string &nobody, bool change_group)
Revokes special privileges (root or suid) at startup, possibly including extra group membership,...
static void beSpecial(Identity special_id, bool change_group=true)
Re-acquires special privileges (either root or suid).
static std::string strerror(int errno_)
Translates an 'errno' value into a meaningful diagnostic string.
static void beOrdinaryForExec(Identity run_as_id) noexcept
Sets the real and effective user-id and group-ids to those given, on a best-effort basis.
static void setEffectiveGroup(Identity)
Sets the effective group-id. Throws on error.
static std::string cwd(bool no_throw=false)
Returns the current working directory.
static void closeStderr()
Closes stderr and reopens it to the null device.
static void beOrdinary(Identity ordinary_id, bool change_group)
Releases special privileges.
static std::string exe()
Returns the absolute path of the current executable, independent of the argv array passed to main().
static int errno_(const SignalSafe &=G::SignalSafe()) noexcept
Returns the process's current 'errno' value.
static void setEffectiveUser(Identity)
Sets the effective user-id. Throws on error.
static void closeFiles(bool keep_stderr=false)
Closes all open file descriptors and reopen stdin, stdout and possibly stderr to the null device.
static void cd(const Path &dir)
Changes directory.
An empty structure that is used to indicate a signal-safe, reentrant implementation.
Definition: gsignalsafe.h:37
static bool isPrintableAscii(string_view s) noexcept
Returns true if every character is a 7-bit, non-control character (ie.
Definition: gstr.cpp:416
static std::string lower(string_view)
Returns a copy of 's' in which all seven-bit upper-case characters have been replaced by lower-case c...
Definition: gstr.cpp:827
Low-level classes.
Definition: garg.h:30
constexpr const char * tx(const char *p)
A briefer alternative to G::gettext_noop().
Definition: ggettext.h:84
STL namespace.
A set of compile-time buffer sizes.
Definition: glimits.h:48