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
37 namespace 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
64 class G::Process::UmaskImp
65{
66 public:
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
74 void G::Process::cd( const Path & dir )
75{
76 if( ! cd(dir,std::nothrow) )
77 throw CannotChangeDirectory( dir.str() ) ;
78}
79
80 bool G::Process::cd( const Path & dir , std::nothrow_t )
81{
82 return 0 == ::chdir( dir.cstr() ) ;
83}
84
85 void G::Process::closeStderr()
86{
87 ProcessImp::reopen( STDERR_FILENO , ProcessImp::Mode::write_only ) ;
88}
89
90 void 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
104 void G::Process::closeOtherFiles( int fd_keep )
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
118 void G::Process::inheritStandardFiles()
119{
120 ProcessImp::noCloseOnExec( STDIN_FILENO ) ;
121 ProcessImp::noCloseOnExec( STDOUT_FILENO ) ;
122 ProcessImp::noCloseOnExec( STDERR_FILENO ) ;
123}
124
125 int G::Process::errno_( const SignalSafe & ) noexcept
126{
127 return errno ; // possible macro, not ::errno or std::errno
128}
129
130 void G::Process::errno_( int e_new ) noexcept
131{
132 errno = e_new ;
133}
134
135 int 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
157 void G::Process::beSpecial( Identity special_identity , bool change_group )
158{
159 ProcessImp::beSpecial( special_identity , change_group ) ;
160}
161
162 void 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
186 void G::Process::beOrdinary( Identity ordinary_id , bool change_group )
187{
188 ProcessImp::beOrdinary( ordinary_id , change_group ) ;
189}
190
191 void G::Process::beOrdinaryForExec( Identity run_as_id ) noexcept
192{
193 ProcessImp::beOrdinaryForExec( run_as_id ) ;
194}
195
196#ifndef G_LIB_SMALL
197 void G::Process::setEffectiveUser( Identity id )
198{
199 G::ProcessImp::setEffectiveUser( id ) ;
200}
201#endif
202
203#ifndef G_LIB_SMALL
204 void G::Process::setEffectiveGroup( Identity id )
205{
206 G::ProcessImp::setEffectiveGroup( id ) ;
207}
208#endif
209
210 G::Path G::Process::cwd()
211{
212 return cwdImp( false ) ;
213}
214
215 G::Path G::Process::cwd( std::nothrow_t )
216{
217 return cwdImp( true ) ;
218}
219
220 G::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>
247 G::Path G::Process::exe()
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
265 G::Path G::Process::exe()
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
291 bool G::Process::Id::operator==( const Id & other ) const noexcept
292{
293 return m_pid == other.m_pid ;
294}
295#endif
296
297 bool G::Process::Id::operator!=( const Id & other ) const noexcept
298{
299 return m_pid != other.m_pid ;
300}
301
302// ==
303
304 void 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
342 void G::Process::Umask::set( Mode mode )
343{
344 UmaskImp::set( mode ) ;
345}
346
347#ifndef G_LIB_SMALL
348 void G::Process::Umask::tightenOther()
349{
350 set( Mode::TightenOther ) ;
351}
352#endif
353
354#ifndef G_LIB_SMALL
355 void G::Process::Umask::loosenGroup()
356{
357 set( Mode::LoosenGroup ) ;
358}
359#endif
360
361// ==
362
363 void G::ProcessImp::noCloseOnExec( int fd ) noexcept
364{
365 ::fcntl( fd , F_SETFD , 0 ) ;
366}
367
368 void 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
389 bool 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
398 G::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
434 void 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
446 void G::ProcessImp::beSpecial( Identity special_identity , bool change_group )
447{
448 setEffectiveUser( special_identity ) ;
449 if( change_group )
450 setEffectiveGroup( special_identity ) ;
451}
452
453 void 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
460 void 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
470 bool G::ProcessImp::setRealUser( Identity id , std::nothrow_t ) noexcept
471{
472 return 0 == ::setuid( id.userid() ) ;
473}
474
475 void 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
484 bool G::ProcessImp::setEffectiveUser( Identity id , std::nothrow_t ) noexcept
485{
486 return 0 == ::seteuid( id.userid() ) ;
487}
488
489 bool G::ProcessImp::setRealGroup( Identity id , std::nothrow_t ) noexcept
490{
491 return 0 == ::setgid( id.groupid() ) ;
492}
493
494 void 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
503 bool G::ProcessImp::setEffectiveGroup( Identity id , std::nothrow_t ) noexcept
504{
505 return 0 == ::setegid( id.groupid() ) ;
506}
507
508 void 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
G::File::open
static void open(std::ofstream &, const Path &)
Calls open() on the given output file stream.
Definition: gfile_unix.cpp:56
G::File::readlink
static G::Path readlink(const Path &link)
Reads a symlink. Throws on error.
Definition: gfile_unix.cpp:436
G::Identity
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
G::Identity::isRoot
bool isRoot() const noexcept
Returns true if the userid is zero.
Definition: gidentity_unix.cpp:116
G::Identity::invalid
static Identity invalid() noexcept
Returns an invalid identity.
Definition: gidentity_unix.cpp:80
G::Identity::root
static Identity root() noexcept
Returns the superuser identity.
Definition: gidentity_unix.cpp:92
G::Identity::effective
static Identity effective() noexcept
Returns the current effective identity.
Definition: gidentity_unix.cpp:70
G::Identity::real
static Identity real() noexcept
Returns the calling process's real identity.
Definition: gidentity_unix.cpp:75
G::Path
A Path object represents a file system path.
Definition: gpath.h:82
G::Path::cstr
const value_type * cstr() const noexcept
Returns the path's c-string.
Definition: gpath.h:249
G::Path::nullDevice
static Path nullDevice()
Returns the path of the "/dev/null" special file, or equivalent.
Definition: gpath.cpp:314
G::Path::str
std::string str() const
Returns the path string.
Definition: gpath.h:243
G::Process::beSpecialForExit
static void beSpecialForExit(SignalSafe, Identity special_id) noexcept
A signal-safe version of beSpecial() that should only be used just before process exit.
Definition: gprocess_unix.cpp:162
G::Process::closeOtherFiles
static void closeOtherFiles(int fd_keep=-1)
Closes all open file descriptors except the three standard ones and optionally one other.
Definition: gprocess_unix.cpp:104
G::Process::cwd
static Path cwd()
Returns the current working directory. Throws on error.
Definition: gprocess_unix.cpp:210
G::Process::beOrdinaryAtStartup
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,...
Definition: gprocess_unix.cpp:167
G::Process::beSpecial
static void beSpecial(Identity special_id, bool change_group=true)
Re-acquires special privileges (either root or suid).
Definition: gprocess_unix.cpp:157
G::Process::strerror
static std::string strerror(int errno_)
Translates an 'errno' value into a meaningful diagnostic string.
Definition: gprocess_unix.cpp:142
G::Process::beOrdinaryForExec
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.
Definition: gprocess_unix.cpp:191
G::Process::exe
static Path exe()
Returns the absolute path of the current executable, independent of the argv array passed to main().
Definition: gprocess_unix.cpp:265
G::Process::setEffectiveGroup
static void setEffectiveGroup(Identity)
Sets the effective group-id. Throws on error.
Definition: gprocess_unix.cpp:204
G::Process::closeStderr
static void closeStderr()
Closes stderr and reopens it to the null device.
Definition: gprocess_unix.cpp:85
G::Process::inheritStandardFiles
static void inheritStandardFiles()
Makes sure that the standard file descriptors are inherited.
Definition: gprocess_unix.cpp:118
G::Process::beOrdinary
static void beOrdinary(Identity ordinary_id, bool change_group)
Releases special privileges.
Definition: gprocess_unix.cpp:186
G::Process::errno_
static int errno_(const SignalSafe &=G::SignalSafe()) noexcept
Returns the process's current 'errno' value.
Definition: gprocess_unix.cpp:125
G::Process::setEffectiveUser
static void setEffectiveUser(Identity)
Sets the effective user-id. Throws on error.
Definition: gprocess_unix.cpp:197
G::Process::closeFiles
static void closeFiles(bool keep_stderr=false)
Closes all open file descriptors and reopens stdin, stdout and possibly stderr to the null device.
Definition: gprocess_unix.cpp:90
G::Process::cd
static void cd(const Path &dir)
Changes directory.
Definition: gprocess_unix.cpp:74
G::Process::errorMessage
static std::string errorMessage(DWORD error)
Translates a GetLastError() value into a meaningful diagnostic string.
Definition: gprocess_unix.cpp:151
G::SignalSafe
An empty structure that is used to indicate a signal-safe, reentrant implementation.
Definition: gsignalsafe.h:37
G::Str::lower
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
G::Str::isPrintableAscii
static bool isPrintableAscii(std::string_view s) noexcept
Returns true if every character is between 0x20 and 0x7e inclusive.
Definition: gstr.cpp:413
gfile.h
gidentity.h
glimits.h
glog.h
gpath.h
gprocess.h
gstr.h
G
Low-level classes.
Definition: garg.h:36
G::tx
constexpr const char * tx(const char *p) noexcept
A briefer alternative to G::gettext_noop().
Definition: ggettext.h:84
std
STL namespace.
G::Limits
A set of compile-time buffer sizes.
Definition: glimits.h:48