E-MailRelay
gprocess_unix.cpp
Go to the documentation of this file.
1//
2// Copyright (C) 2001-2024 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 enum class Mode { read_only , write_only } ;
45 void reopen( int fd , Mode mode ) ;
46 mode_t umaskValue( G::Process::Umask::Mode mode ) ;
47 bool readlink_( std::string_view path , std::string & value ) ;
48 bool setRealUser( Identity id , std::nothrow_t ) noexcept ;
49 bool setRealGroup( Identity id , std::nothrow_t ) noexcept ;
50 void setEffectiveUser( Identity id ) ;
51 bool setEffectiveUser( Identity id , std::nothrow_t ) noexcept ;
52 void setEffectiveGroup( Identity id ) ;
53 bool setEffectiveGroup( Identity id , std::nothrow_t ) noexcept ;
54 void throwError() ;
55 void beSpecial( Identity special_identity , bool change_group ) ;
56 void beSpecialForExit( SignalSafe , Identity special_identity ) noexcept ;
57 Identity beOrdinaryAtStartup( Identity , bool change_group ) ;
58 Identity beOrdinary( Identity , bool change_group ) ;
59 void beOrdinaryForExec( Identity run_as_id ) noexcept ;
60 void revokeExtraGroups() ;
61 }
62}
63
64class G::Process::UmaskImp
65{
66public:
67 mode_t m_old_mode ;
68 static mode_t set( Process::Umask::Mode ) noexcept ;
69 static void set( mode_t ) noexcept ;
70} ;
71
72// ==
73
74void G::Process::cd( const Path & dir )
75{
76 if( ! cd(dir,std::nothrow) )
77 throw CannotChangeDirectory( dir.str() ) ;
78}
79
80bool G::Process::cd( const Path & dir , std::nothrow_t )
81{
82 return 0 == ::chdir( dir.cstr() ) ;
83}
84
86{
87 ProcessImp::reopen( STDERR_FILENO , ProcessImp::Mode::write_only ) ;
88}
89
90void G::Process::closeFiles( bool keep_stderr )
91{
92 std::cout << std::flush ;
93 std::cerr << std::flush ;
94
95 ProcessImp::reopen( STDIN_FILENO , ProcessImp::Mode::read_only ) ;
96 ProcessImp::reopen( STDOUT_FILENO , ProcessImp::Mode::write_only ) ;
97 if( !keep_stderr )
98 ProcessImp::reopen( STDERR_FILENO , ProcessImp::Mode::write_only ) ;
99
100 closeOtherFiles() ;
101 inheritStandardFiles() ;
102}
103
105{
106 int n = 256U ;
107 long rc = ::sysconf( _SC_OPEN_MAX ) ;
108 if( rc > 0L )
109 n = static_cast<int>( rc ) ;
110
111 for( int fd = 0 ; fd < n ; fd++ )
112 {
113 if( fd != STDIN_FILENO && fd != STDOUT_FILENO && fd != STDERR_FILENO && fd != fd_keep )
114 ::close( fd ) ;
115 }
116}
117
119{
120 ProcessImp::noCloseOnExec( STDIN_FILENO ) ;
121 ProcessImp::noCloseOnExec( STDOUT_FILENO ) ;
122 ProcessImp::noCloseOnExec( STDERR_FILENO ) ;
123}
124
125int G::Process::errno_( const SignalSafe & ) noexcept
126{
127 return errno ; // possible macro, not ::errno or std::errno
128}
129
130void G::Process::errno_( int e_new ) noexcept
131{
132 errno = e_new ;
133}
134
135int G::Process::errno_( const SignalSafe & , int e_new ) noexcept
136{
137 int e_old = errno ;
138 errno = e_new ;
139 return e_old ;
140}
141
142std::string G::Process::strerror( int errno_ )
143{
144 char * p = std::strerror( errno_ ) ;
145 std::string s( p ? p : "" ) ;
146 if( s.empty() ) s = "unknown error" ;
147 return Str::isPrintableAscii(s) ? Str::lower(s) : s ;
148}
149
150#ifndef G_LIB_SMALL
151std::string G::Process::errorMessage( DWORD e )
152{
153 return std::string("error ").append( std::to_string(e) ) ;
154}
155#endif
156
157void G::Process::beSpecial( Identity special_identity , bool change_group )
158{
159 ProcessImp::beSpecial( special_identity , change_group ) ;
160}
161
162void G::Process::beSpecialForExit( SignalSafe , Identity special_identity ) noexcept
163{
164 ProcessImp::beSpecialForExit( SignalSafe() , special_identity ) ;
165}
166
167std::pair<G::Identity,G::Identity> G::Process::beOrdinaryAtStartup( const std::string & ordinary_name ,
168 bool change_group )
169{
170 Identity ordinary_id( ordinary_name ) ;
171
172 // revoke extra groups, but not if we are leaving groups alone
173 // or we have been given root as the non-root user or we
174 // are running vanilla
175 if( change_group && !ordinary_id.isRoot() )
176 {
177 Identity real = Identity::real() ;
178 if( real.isRoot() || real != Identity::effective() )
179 ProcessImp::revokeExtraGroups() ;
180 }
181
182 Identity startup_id = ProcessImp::beOrdinary( ordinary_id , change_group ) ;
183 return { ordinary_id , startup_id } ;
184}
185
186void G::Process::beOrdinary( Identity ordinary_id , bool change_group )
187{
188 ProcessImp::beOrdinary( ordinary_id , change_group ) ;
189}
190
191void G::Process::beOrdinaryForExec( Identity run_as_id ) noexcept
192{
193 ProcessImp::beOrdinaryForExec( run_as_id ) ;
194}
195
196#ifndef G_LIB_SMALL
198{
199 G::ProcessImp::setEffectiveUser( id ) ;
200}
201#endif
202
203#ifndef G_LIB_SMALL
205{
206 G::ProcessImp::setEffectiveGroup( id ) ;
207}
208#endif
209
211{
212 return cwdImp( false ) ;
213}
214
215G::Path G::Process::cwd( std::nothrow_t )
216{
217 return cwdImp( true ) ;
218}
219
220G::Path G::Process::cwdImp( bool no_throw )
221{
222 std::string result ;
223 std::array<std::size_t,2U> sizes = {{ G::Limits<>::path_buffer , PATH_MAX+1U }} ;
224 for( std::size_t n : sizes )
225 {
226 std::vector<char> buffer( n ) ;
227 char * p = getcwd( buffer.data() , buffer.size() ) ;
228 int error = errno_() ;
229 if( p != nullptr )
230 {
231 buffer.push_back( '\0' ) ;
232 result.assign( buffer.data() ) ;
233 break ;
234 }
235 else if( error != ERANGE )
236 {
237 break ;
238 }
239 }
240 if( result.empty() && !no_throw )
241 throw GetCwdError() ;
242 return {result} ;
243}
244
245#ifdef G_UNIX_MAC
246#include <libproc.h>
248{
249 // (see also _NSGetExecutablePath())
250 std::vector<char> buffer( std::max(100,PROC_PIDPATHINFO_MAXSIZE) ) ;
251 buffer[0] = '\0' ;
252 int rc = proc_pidpath( getpid() , buffer.data() , buffer.size() ) ;
253 if( rc > 0 )
254 {
255 std::size_t n = static_cast<std::size_t>(rc) ;
256 if( n > buffer.size() ) n = buffer.size() ;
257 return Path( std::string_view(buffer.data(),n) ) ;
258 }
259 else
260 {
261 return {} ;
262 }
263}
264#else
266{
267 // best effort, not guaranteed
268 std::string result ;
269 ProcessImp::readlink_( "/proc/self/exe" , result ) ||
270 ProcessImp::readlink_( "/proc/curproc/file" , result ) ||
271 ProcessImp::readlink_( "/proc/curproc/exe" , result ) ;
272 return {result} ;
273}
274#endif
275
276// ==
277
278G::Process::Id::Id() noexcept :
279 m_pid(::getpid())
280{
281}
282
283std::string G::Process::Id::str() const
284{
285 std::ostringstream ss ;
286 ss << m_pid ;
287 return ss.str() ;
288}
289
290#ifndef G_LIB_SMALL
291bool G::Process::Id::operator==( const Id & other ) const noexcept
292{
293 return m_pid == other.m_pid ;
294}
295#endif
296
297bool G::Process::Id::operator!=( const Id & other ) const noexcept
298{
299 return m_pid != other.m_pid ;
300}
301
302// ==
303
304void G::Process::UmaskImp::set( mode_t mode ) noexcept
305{
306 GDEF_IGNORE_RETURN ::umask( mode ) ;
307}
308
309mode_t G::Process::UmaskImp::set( Umask::Mode mode ) noexcept
310{
311 mode_t old = ::umask( 2 ) ;
312 mode_t new_ = old ;
313 if( mode == Umask::Mode::Tightest )
314 new_ = 0077 ; // -rw-------
315 else if( mode == Umask::Mode::Tighter )
316 new_ = 0007 ; // -rw-rw----
317 else if( mode == Umask::Mode::Readable )
318 new_ = 0022 ; // -rw-r--r--
319 else if( mode == Umask::Mode::GroupOpen )
320 new_ = 0002 ; // -rw-rw-r--
321 else if( mode == Umask::Mode::TightenOther )
322 new_ = old | mode_t(007) ;
323 else if( mode == Umask::Mode::LoosenGroup )
324 new_ = old & ~mode_t(070) ;
325 else if( mode == Umask::Mode::Open )
326 new_ = 0 ; // -rw-rw-rw-
327 set( new_ ) ;
328 return old ;
329}
330
331G::Process::Umask::Umask( Mode mode ) :
332 m_imp(std::make_unique<UmaskImp>())
333{
334 m_imp->m_old_mode = UmaskImp::set( mode ) ;
335}
336
337G::Process::Umask::~Umask()
338{
339 UmaskImp::set( m_imp->m_old_mode ) ;
340}
341
342void G::Process::Umask::set( Mode mode )
343{
344 UmaskImp::set( mode ) ;
345}
346
347#ifndef G_LIB_SMALL
348void G::Process::Umask::tightenOther()
349{
350 set( Mode::TightenOther ) ;
351}
352#endif
353
354#ifndef G_LIB_SMALL
355void G::Process::Umask::loosenGroup()
356{
357 set( Mode::LoosenGroup ) ;
358}
359#endif
360
361// ==
362
363void G::ProcessImp::noCloseOnExec( int fd ) noexcept
364{
365 ::fcntl( fd , F_SETFD , 0 ) ;
366}
367
368void G::ProcessImp::reopen( int fd , Mode mode_in )
369{
370 auto mode = mode_in == Mode::read_only ? File::InOutAppend::In : File::InOutAppend::OutNoCreate ;
371 int fd_null = File::open( Path::nullDevice() , mode ) ;
372 if( fd_null < 0 ) throw DevNullError() ;
373 ::dup2( fd_null , fd ) ;
374 ::close( fd_null ) ;
375}
376
377#ifndef G_LIB_SMALL
378mode_t G::ProcessImp::umaskValue( Process::Umask::Mode mode )
379{
380 mode_t m = 0 ;
381 if( mode == Process::Umask::Mode::Tightest ) m = 0177 ; // -rw-------
382 if( mode == Process::Umask::Mode::Tighter ) m = 0117 ; // -rw-rw----
383 if( mode == Process::Umask::Mode::Readable ) m = 0133 ; // -rw-r--r--
384 if( mode == Process::Umask::Mode::GroupOpen ) m = 0113 ;// -rw-rw-r--
385 return m ;
386}
387#endif
388
389bool G::ProcessImp::readlink_( std::string_view path , std::string & value )
390{
391 Path target = File::readlink( Path(path) , std::nothrow ) ;
392 if( !target.empty() ) value = target.str() ;
393 return !target.empty() ;
394}
395
396// ==
397
398G::Identity G::ProcessImp::beOrdinary( Identity nobody_id , bool change_group )
399{
400 Identity old_id = Identity::effective() ;
401 Identity real_id = Identity::real() ;
402 if( real_id.isRoot() )
403 {
404 if( change_group )
405 {
406 // make sure we have privilege to change group
407 if( !setEffectiveUser( Identity::root() , std::nothrow ) )
408 throwError() ;
409
410 if( !setEffectiveGroup( nobody_id , std::nothrow ) )
411 {
412 setEffectiveUser( old_id , std::nothrow ) ; // rollback
413 throwError() ;
414 }
415 }
416 if( !setEffectiveUser( nobody_id , std::nothrow ) )
417 throwError() ;
418 }
419 else
420 {
421 // change to real id -- drops suid privileges
422 if( !setEffectiveUser( real_id , std::nothrow ) )
423 throwError() ;
424
425 if( change_group && !setEffectiveGroup( real_id , std::nothrow ) )
426 {
427 setEffectiveUser( old_id , std::nothrow ) ; // rollback
428 throwError() ;
429 }
430 }
431 return old_id ;
432}
433
434void G::ProcessImp::beOrdinaryForExec( Identity run_as_id ) noexcept
435{
436 if( run_as_id != Identity::invalid() )
437 {
438 setEffectiveUser( Identity::root() , std::nothrow ) ; // for root-suid
439 setRealGroup( run_as_id , std::nothrow ) ;
440 setEffectiveGroup( run_as_id , std::nothrow ) ;
441 setRealUser( run_as_id , std::nothrow ) ;
442 setEffectiveUser( run_as_id , std::nothrow ) ;
443 }
444}
445
446void G::ProcessImp::beSpecial( Identity special_identity , bool change_group )
447{
448 setEffectiveUser( special_identity ) ;
449 if( change_group )
450 setEffectiveGroup( special_identity ) ;
451}
452
453void G::ProcessImp::beSpecialForExit( SignalSafe , Identity special_identity ) noexcept
454{
455 // changing effective ids is not strictly signal-safe :-<
456 setEffectiveUser( special_identity , std::nothrow ) ;
457 setEffectiveGroup( special_identity , std::nothrow ) ;
458}
459
460void G::ProcessImp::revokeExtraGroups()
461{
462 if( Identity::real().isRoot() || Identity::effective() != Identity::real() )
463 {
464 // set supplementary group-ids to a zero-length list
465 gid_t dummy = 0 ;
466 GDEF_IGNORE_RETURN ::setgroups( 0U , &dummy ) ; // (only works for root, so ignore the return code)
467 }
468}
469
470bool G::ProcessImp::setRealUser( Identity id , std::nothrow_t ) noexcept
471{
472 return 0 == ::setuid( id.userid() ) ;
473}
474
475void G::ProcessImp::setEffectiveUser( Identity id )
476{
477 if( ::seteuid(id.userid()) )
478 {
479 int e = errno ;
480 throw Process::UidError( Process::strerror(e) ) ;
481 }
482}
483
484bool G::ProcessImp::setEffectiveUser( Identity id , std::nothrow_t ) noexcept
485{
486 return 0 == ::seteuid( id.userid() ) ;
487}
488
489bool G::ProcessImp::setRealGroup( Identity id , std::nothrow_t ) noexcept
490{
491 return 0 == ::setgid( id.groupid() ) ;
492}
493
494void G::ProcessImp::setEffectiveGroup( Identity id )
495{
496 if( ::setegid(id.groupid()) )
497 {
498 int e = errno ;
499 throw Process::GidError( Process::strerror(e) ) ;
500 }
501}
502
503bool G::ProcessImp::setEffectiveGroup( Identity id , std::nothrow_t ) noexcept
504{
505 return 0 == ::setegid( id.groupid() ) ;
506}
507
508void G::ProcessImp::throwError()
509{
510 // typically we are about to std::terminate() so make sure there is an error message
511 G_ERROR( "G::ProcessImp::throwError: failed to change process identity" ) ;
512 throw IdentityError() ;
513}
514
static void open(std::ofstream &, const Path &)
Calls open() on the given output file stream.
Definition: gfile_unix.cpp:56
static G::Path readlink(const Path &link)
Reads a symlink. Throws on error.
Definition: gfile_unix.cpp:436
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:45
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:82
const value_type * cstr() const noexcept
Returns the path's c-string.
Definition: gpath.h:249
static Path nullDevice()
Returns the path of the "/dev/null" special file, or equivalent.
Definition: gpath.cpp:314
std::string str() const
Returns the path string.
Definition: gpath.h:243
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 optionally one other.
static Path cwd()
Returns the current working directory. Throws on error.
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 Path exe()
Returns the absolute path of the current executable, independent of the argv array passed to main().
static void setEffectiveGroup(Identity)
Sets the effective group-id. Throws on error.
static void closeStderr()
Closes stderr and reopens it to the null device.
static void inheritStandardFiles()
Makes sure that the standard file descriptors are inherited.
static void beOrdinary(Identity ordinary_id, bool change_group)
Releases special privileges.
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 reopens stdin, stdout and possibly stderr to the null device.
static void cd(const Path &dir)
Changes directory.
static std::string errorMessage(DWORD error)
Translates a GetLastError() value into a meaningful diagnostic string.
An empty structure that is used to indicate a signal-safe, reentrant implementation.
Definition: gsignalsafe.h:37
static std::string lower(std::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:824
static bool isPrintableAscii(std::string_view s) noexcept
Returns true if every character is between 0x20 and 0x7e inclusive.
Definition: gstr.cpp:413
Low-level classes.
Definition: garg.h:36
constexpr const char * tx(const char *p) noexcept
A briefer alternative to G::gettext_noop().
Definition: ggettext.h:84
STL namespace.
A set of compile-time buffer sizes.
Definition: glimits.h:48