1 /*---------------------------------------------------------------------\
3 | |__ / \ / / . \ . \ |
8 \---------------------------------------------------------------------*/
9 /** \file zypp/misc/CheckAccessDeleted.cc
14 #include <unordered_set>
17 #include <zypp/base/LogControl.h>
18 #include <zypp/base/LogTools.h>
19 #include <zypp/base/String.h>
20 #include <zypp/base/Gettext.h>
21 #include <zypp/base/Exception.h>
23 #include <zypp/PathInfo.h>
24 #include <zypp/ExternalProgram.h>
25 #include <zypp/base/Regex.h>
26 #include <zypp/base/IOStream.h>
27 #include <zypp/base/InputStream.h>
28 #include <zypp/target/rpm/librpmDb.h>
30 #include <zypp/misc/CheckAccessDeleted.h>
34 #undef ZYPP_BASE_LOGGER_LOGGROUP
35 #define ZYPP_BASE_LOGGER_LOGGROUP "zypp::misc"
37 ///////////////////////////////////////////////////////////////////
39 { /////////////////////////////////////////////////////////////////
41 ///////////////////////////////////////////////////////////////////
43 { /////////////////////////////////////////////////////////////////
45 // lsof output lines are a sequence of NUL terminated fields,
46 // where the 1st char determines the fields type.
48 // (pcuL) pid command userid loginname
49 // (ftkn).filedescriptor type linkcount filename
51 /////////////////////////////////////////////////////////////////
53 /** lsof output line + files extracted so far for this PID */
54 typedef std::pair<std::string,std::unordered_set<std::string>> CacheEntry;
56 /////////////////////////////////////////////////////////////////
57 /// \class FilterRunsInContainer
58 /// \brief Functor guessing whether \a PID is running in a container.
60 /// Use /proc to guess if a process is running in a container
61 /////////////////////////////////////////////////////////////////
62 struct FilterRunsInContainer
73 * Checks if the given file in proc is part of our root
74 * or not. If the file was unlinked IGNORE is returned to signal
75 * that its better to check the next file.
77 Type in_our_root( const Pathname &path ) const {
79 const PathInfo procInfoStat( path );
81 // if we can not stat the file continue to the next one
82 if ( procInfoStat.error() ) return IGNORE;
84 // if the file was unlinked ignore it
85 if ( procInfoStat.nlink() == 0 )
88 // get the file the link points to, if that fails continue to the next
89 const Pathname linkTarget = filesystem::readlink( path );
90 if ( linkTarget.empty() ) return IGNORE;
92 // get stat info for the target file
93 const PathInfo linkStat( linkTarget );
95 // Non-existent path means it's not reachable by us.
96 if ( !linkStat.isExist() )
99 // If the file exists, it could simply mean it exists in and outside a container, check inode to be safe
100 if ( linkStat.ino() != procInfoStat.ino())
103 // If the inode is the same, it could simply mean it exists in and outside a container but on different devices, check to be safe
104 if ( linkStat.dev() != procInfoStat.dev() )
107 // assume HOST if all tests fail
114 * Iterates over the /proc contents for the given pid
116 bool operator()( const pid_t pid ) const {
118 // first check the exe file
119 const Pathname pidDir = Pathname("/proc") / asString(pid);
120 const Pathname exeFile = pidDir / "exe";
122 auto res = in_our_root( exeFile );
124 return res == CONTAINER;
126 // if IGNORE was returned we need to continue testing all the files in /proc/<pid>/map_files until we hopefully
127 // find a still existing file. If all tests fail we will simply assume this pid is running on the HOST
129 // a map of all already tested files, each file can be mapped multiple times and we do not want to check them more than once
130 std::unordered_set<std::string> tested;
132 // iterate over all the entries in /proc/<pid>/map_files
133 filesystem::dirForEach( pidDir / "map_files", [ this, &tested, &res ]( const Pathname & dir_r, const char *const & name_r ){
135 // some helpers to make the code more self explanatory
136 constexpr bool contloop = true;
137 constexpr bool stoploop = false;
139 const Pathname entryName = dir_r / name_r;
141 // get the links target file and check if we alreadys know it, also if we can not read link information we skip the file
142 const Pathname linkTarget = filesystem::readlink( entryName );
143 if ( linkTarget.empty() || !tested.insert( linkTarget.asString() ).second ) return contloop;
145 // try to get file type
146 const auto mappedFileType = in_our_root( entryName );
148 // if we got something, remember the value and stop the loop
149 if ( mappedFileType > IGNORE ) {
150 res = mappedFileType;
156 // if res is still IGNORE we did not find a explicit answer. So to be safe we assume its running on the host
158 return false; // can't tell for sure, lets assume host
160 return res == CONTAINER;
163 FilterRunsInContainer() {}
167 /** bsc#1099847: Check for lsof version < 4.90 which does not support '-K i'
168 * Just a quick check to allow code15 libzypp runnig in a code12 environment.
169 * bsc#1036304: '-K i' was backported to older lsof versions, indicated by
170 * lsof providing 'backported-option-Ki'.
174 using target::rpm::librpmDb;
175 // RpmDb access is blocked while the Target is not initialized.
176 // Launching the Target just for this query would be an overkill.
179 : _wasBlocked( librpmDb::isBlocked() )
180 { if ( _wasBlocked ) librpmDb::unblockAccess(); }
182 { if ( _wasBlocked ) librpmDb::blockAccess(); }
187 librpmDb::db_const_iterator it;
188 return( it.findPackage( "lsof" ) && it->tag_edition() < Edition("4.90") && !it->tag_provides().count( Capability("backported-option-Ki") ) );
192 /////////////////////////////////////////////////////////////////
194 class CheckAccessDeleted::Impl
197 CheckAccessDeleted::Impl *clone() const;
199 bool addDataIf( const CacheEntry & cache_r, std::vector<std::string> *debMap = nullptr );
200 void addCacheIf( CacheEntry & cache_r, const std::string & line_r, std::vector<std::string> *debMap = nullptr );
202 std::map<pid_t,CacheEntry> filterInput( externalprogram::ExternalDataSource &source );
203 CheckAccessDeleted::size_type createProcInfo( const std::map<pid_t,CacheEntry> &in );
205 std::vector<CheckAccessDeleted::ProcInfo> _data;
206 bool _fromLsofFileMode = false; // Set if we currently process data from a debug file
207 bool _verbose = false;
209 std::map<pid_t,std::vector<std::string>> debugMap; //will contain all used lsof files after filtering
213 CheckAccessDeleted::Impl *CheckAccessDeleted::Impl::clone() const
215 Impl *myClone = new Impl( *this );
219 /** Add \c cache to \c data if the process is accessing deleted files.
220 * \c pid string in \c cache is the proc line \c (pcuLR), \c files
221 * are already in place. Always clear the \c cache.files!
223 inline bool CheckAccessDeleted::Impl::addDataIf( const CacheEntry & cache_r, std::vector<std::string> *debMap )
225 const auto & filelist( cache_r.second );
227 if ( filelist.empty() )
230 // at least one file access so keep it:
231 _data.push_back( CheckAccessDeleted::ProcInfo() );
232 CheckAccessDeleted::ProcInfo & pinfo( _data.back() );
233 pinfo.files.insert( pinfo.files.begin(), filelist.begin(), filelist.end() );
235 const std::string & pline( cache_r.first );
236 std::string commandname; // pinfo.command if still needed...
237 std::ostringstream pLineStr; //rewrite the first line in debug cache
238 for_( ch, pline.begin(), pline.end() )
243 pinfo.pid = &*(ch+1);
245 pLineStr <<&*(ch)<<'\0';
248 pinfo.ppid = &*(ch+1);
250 pLineStr <<&*(ch)<<'\0';
253 pinfo.puid = &*(ch+1);
255 pLineStr <<&*(ch)<<'\0';
258 pinfo.login = &*(ch+1);
260 pLineStr <<&*(ch)<<'\0';
263 if ( pinfo.command.empty() ) {
264 commandname = &*(ch+1);
265 // the lsof command name might be truncated, so we prefer /proc/<pid>/exe
266 if (!_fromLsofFileMode)
267 pinfo.command = filesystem::readlink( Pathname("/proc")/pinfo.pid/"exe" ).basename();
268 if ( pinfo.command.empty() )
269 pinfo.command = std::move(commandname);
271 pLineStr <<'c'<<pinfo.command<<'\0';
275 if ( *ch == '\n' ) break; // end of data
276 do { ++ch; } while ( *ch != '\0' ); // skip to next field
279 //replace the data in the debug cache as well
282 debMap->front() = pLineStr.str();
290 /** Add file to cache if it refers to a deleted executable or library file:
291 * - Either the link count \c(k) is \c 0, or no link cout is present.
292 * - The type \c (t) is set to \c REG or \c DEL
293 * - The filedescriptor \c (f) is set to \c txt, \c mem or \c DEL
295 inline void CheckAccessDeleted::Impl::addCacheIf( CacheEntry & cache_r, const std::string & line_r, std::vector<std::string> *debMap )
301 for_( ch, line_r.c_str(), ch+line_r.size() )
306 if ( *(ch+1) != '0' ) // skip non-zero link counts
319 if ( *ch == '\n' ) break; // end of data
320 do { ++ch; } while ( *ch != '\0' ); // skip to next field
323 if ( !t || !f || !n )
324 return; // wrong filedescriptor/type/name
326 if ( !( ( *t == 'R' && *(t+1) == 'E' && *(t+2) == 'G' && *(t+3) == '\0' )
327 || ( *t == 'D' && *(t+1) == 'E' && *(t+2) == 'L' && *(t+3) == '\0' ) ) )
328 return; // wrong type
330 if ( !( ( *f == 'm' && *(f+1) == 'e' && *(f+2) == 'm' && *(f+3) == '\0' )
331 || ( *f == 't' && *(f+1) == 'x' && *(f+2) == 't' && *(f+3) == '\0' )
332 || ( *f == 'D' && *(f+1) == 'E' && *(f+2) == 'L' && *(f+3) == '\0' )
333 || ( *f == 'l' && *(f+1) == 't' && *(f+2) == 'x' && *(f+3) == '\0' ) ) )
334 return; // wrong filedescriptor type
336 if ( str::contains( n, "(stat: Permission denied)" ) )
337 return; // Avoid reporting false positive due to insufficient permission.
341 if ( ! ( str::contains( n, "/lib" ) || str::contains( n, "bin/" ) ) )
342 return; // Try to avoid reporting false positive unless verbose.
345 if ( *f == 'm' || *f == 'D' ) // skip some wellknown nonlibrary memorymapped files
347 static const char * black[] = {
355 for_( it, arrayBegin( black ), arrayEnd( black ) )
357 if ( str::hasPrefix( n, *it ) )
361 // Add if no duplicate
362 if ( debMap && cache_r.second.find(n) == cache_r.second.end() ) {
363 debMap->push_back(line_r);
365 cache_r.second.insert( n );
368 CheckAccessDeleted::CheckAccessDeleted( bool doCheck_r )
371 if ( doCheck_r ) check();
374 CheckAccessDeleted::size_type CheckAccessDeleted::check( const Pathname &lsofOutput_r, bool verbose_r )
376 _pimpl->_verbose = verbose_r;
377 _pimpl->_fromLsofFileMode = true;
379 FILE *inFile = fopen( lsofOutput_r.c_str(), "r" );
381 ZYPP_THROW( Exception( str::Format("Opening input file %1% failed.") % lsofOutput_r.c_str() ) );
384 //inFile is closed by ExternalDataSource
385 externalprogram::ExternalDataSource inSource( inFile, nullptr );
386 auto cache = _pimpl->filterInput( inSource );
387 return _pimpl->createProcInfo( cache );
390 std::map<pid_t,CacheEntry> CheckAccessDeleted::Impl::filterInput( externalprogram::ExternalDataSource &source )
392 // cachemap: PID => (deleted files)
393 // NOTE: omit PIDs running in a (lxc/docker) container
394 std::map<pid_t,CacheEntry> cachemap;
396 bool debugEnabled = !_debugFile.empty();
399 FilterRunsInContainer runsInLXC;
400 MIL << "Silently scanning lsof output..." << endl;
401 zypp::base::LogControl::TmpLineWriter shutUp; // suppress excessive readdir etc. logging in runsInLXC
402 for( std::string line = source.receiveLine( 30 * 1000 ); ! line.empty(); line = source.receiveLine( 30 * 1000 ) )
404 // NOTE: line contains '\0' separeated fields!
405 if ( line[0] == 'p' )
407 str::strtonum( line.c_str()+1, cachepid ); // line is "p<PID>\0...."
408 if ( _fromLsofFileMode || !runsInLXC( cachepid ) ) {
409 if ( debugEnabled ) {
410 auto &pidMad = debugMap[cachepid];
411 if ( pidMad.empty() )
412 debugMap[cachepid].push_back( line );
414 debugMap[cachepid].front() = line;
416 cachemap[cachepid].first.swap( line );
418 cachepid = 0; // ignore this pid
423 auto &dbgMap = debugMap[cachepid];
424 addCacheIf( cachemap[cachepid], line, debugEnabled ? &dbgMap : nullptr);
430 CheckAccessDeleted::size_type CheckAccessDeleted::check( bool verbose_r )
432 static const char* argv[] = { "lsof", "-n", "-FpcuLRftkn0", "-K", "i", NULL };
436 _pimpl->_verbose = verbose_r;
437 _pimpl->_fromLsofFileMode = false;
439 ExternalProgram prog( argv, ExternalProgram::Discard_Stderr );
440 std::map<pid_t,CacheEntry> cachemap;
443 cachemap = _pimpl->filterInput( prog );
444 } catch ( const io::TimeoutException &e ) {
447 ZYPP_THROW ( Exception( "Reading data from 'lsof' timed out.") );
450 int ret = prog.close();
455 ZYPP_THROW( Exception(_("Please install package 'lsof' first.") ) );
457 Exception err( str::Format("Executing 'lsof' failed (%1%).") % ret );
458 err.remember( prog.execError() );
462 return _pimpl->createProcInfo( cachemap );
465 CheckAccessDeleted::size_type CheckAccessDeleted::Impl::createProcInfo(const std::map<pid_t,CacheEntry> &in)
467 std::ofstream debugFileOut;
468 bool debugEnabled = false;
469 if ( !_debugFile.empty() ) {
470 debugFileOut.open( _debugFile.c_str() );
471 debugEnabled = debugFileOut.is_open();
473 if ( !debugEnabled ) {
474 ERR<<"Unable to open debug file: "<<_debugFile<<endl;
479 for ( const auto &cached : in )
482 addDataIf( cached.second);
484 std::vector<std::string> *mapPtr = nullptr;
486 auto dbgInfo = debugMap.find(cached.first);
487 if ( dbgInfo != debugMap.end() )
488 mapPtr = &(dbgInfo->second);
490 if( !addDataIf( cached.second, mapPtr ) )
493 for ( const std::string &dbgLine: dbgInfo->second ) {
494 debugFileOut.write( dbgLine.c_str(), dbgLine.length() );
501 bool CheckAccessDeleted::empty() const
503 return _pimpl->_data.empty();
506 CheckAccessDeleted::size_type CheckAccessDeleted::size() const
508 return _pimpl->_data.size();
511 CheckAccessDeleted::const_iterator CheckAccessDeleted::begin() const
513 return _pimpl->_data.begin();
516 CheckAccessDeleted::const_iterator CheckAccessDeleted::end() const
518 return _pimpl->_data.end();
521 void CheckAccessDeleted::setDebugOutputFile(const Pathname &filename_r)
523 _pimpl->_debugFile = filename_r;
526 std::string CheckAccessDeleted::findService( pid_t pid_r )
529 p.pid = str::numstring( pid_r );
533 std::string CheckAccessDeleted::ProcInfo::service() const
535 static const str::regex rx( "[0-9]+:name=systemd:/system.slice/(.*/)?(.*).service$" );
538 iostr::simpleParseFile( InputStream( Pathname("/proc")/pid/"cgroup" ),
539 [&]( int num_r, std::string line_r )->bool
541 if ( str::regex_match( line_r, what, rx ) )
544 return false; // stop after match
551 /******************************************************************
553 ** FUNCTION NAME : operator<<
554 ** FUNCTION TYPE : std::ostream &
556 std::ostream & operator<<( std::ostream & str, const CheckAccessDeleted & obj )
558 return dumpRange( str << "CheckAccessDeleted ",
563 /******************************************************************
565 ** FUNCTION NAME : operator<<
566 ** FUNCTION TYPE : std::ostream &
568 std::ostream & operator<<( std::ostream & str, const CheckAccessDeleted::ProcInfo & obj )
570 if ( obj.pid.empty() )
571 return str << "<NoProc>";
573 return dumpRangeLine( str << obj.command
583 /////////////////////////////////////////////////////////////////
585 ///////////////////////////////////////////////////////////////////