Imported Upstream version 17.23.5
[platform/upstream/libzypp.git] / zypp / misc / CheckAccessDeleted.cc
1 /*---------------------------------------------------------------------\
2 |                          ____ _   __ __ ___                          |
3 |                         |__  / \ / / . \ . \                         |
4 |                           / / \ V /|  _/  _/                         |
5 |                          / /__ | | | | | |                           |
6 |                         /_____||_| |_| |_|                           |
7 |                                                                      |
8 \---------------------------------------------------------------------*/
9 /** \file       zypp/misc/CheckAccessDeleted.cc
10  *
11 */
12 #include <iostream>
13 #include <fstream>
14 #include <unordered_set>
15 #include <iterator>
16 #include <stdio.h>
17 #include <zypp/base/LogTools.h>
18 #include <zypp/base/String.h>
19 #include <zypp/base/Gettext.h>
20 #include <zypp/base/Exception.h>
21
22 #include <zypp/PathInfo.h>
23 #include <zypp/ExternalProgram.h>
24 #include <zypp/base/Regex.h>
25 #include <zypp/base/IOStream.h>
26 #include <zypp/base/InputStream.h>
27 #include <zypp/target/rpm/librpmDb.h>
28
29 #include <zypp/misc/CheckAccessDeleted.h>
30
31 using std::endl;
32
33 #undef ZYPP_BASE_LOGGER_LOGGROUP
34 #define ZYPP_BASE_LOGGER_LOGGROUP "zypp::misc"
35
36 ///////////////////////////////////////////////////////////////////
37 namespace zypp
38 { /////////////////////////////////////////////////////////////////
39
40   ///////////////////////////////////////////////////////////////////
41   namespace
42   { /////////////////////////////////////////////////////////////////
43     //
44     // lsof output lines are a sequence of NUL terminated fields,
45     // where the 1st char determines the fields type.
46     //
47     // (pcuL) pid command userid loginname
48     // (ftkn).filedescriptor type linkcount filename
49     //
50     /////////////////////////////////////////////////////////////////
51
52     /** lsof output line + files extracted so far for this PID */
53     typedef std::pair<std::string,std::unordered_set<std::string>> CacheEntry;
54
55     /////////////////////////////////////////////////////////////////
56     /// \class FilterRunsInContainer
57     /// \brief Functor guessing whether \a PID is running in a container.
58     ///
59     /// Use /proc to guess if a process is running in a container
60     /////////////////////////////////////////////////////////////////
61     struct FilterRunsInContainer
62     {
63     private:
64
65       enum Type {
66         IGNORE,
67         HOST,
68         CONTAINER
69       };
70
71       /*!
72        * Checks if the given file in proc is part of our root
73        * or not. If the file was unlinked IGNORE is returned to signal
74        * that its better to check the next file.
75        */
76       Type in_our_root( const Pathname &path ) const {
77
78         const PathInfo procInfoStat( path );
79
80         // if we can not stat the file continue to the next one
81         if ( procInfoStat.error() ) return IGNORE;
82
83         // if the file was unlinked ignore it
84         if ( procInfoStat.nlink() == 0 )
85           return IGNORE;
86
87         // get the file the link points to, if that fails continue to the next
88         const Pathname linkTarget = filesystem::readlink( path );
89         if ( linkTarget.empty() ) return IGNORE;
90
91         // get stat info for the target file
92         const PathInfo linkStat( linkTarget );
93
94         // Non-existent path means it's not reachable by us.
95         if ( !linkStat.isExist() )
96           return CONTAINER;
97
98         // If the file exists, it could simply mean it exists in and outside a container, check inode to be safe
99         if ( linkStat.ino() != procInfoStat.ino())
100           return CONTAINER;
101
102         // 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
103         if ( linkStat.dev() != procInfoStat.dev() )
104           return CONTAINER;
105
106         // assume HOST if all tests fail
107         return HOST;
108       }
109
110     public:
111
112       /*!
113        * Iterates over the /proc contents for the given pid
114        */
115       bool operator()( const pid_t pid ) const {
116
117         // first check the exe file
118         const Pathname pidDir  = Pathname("/proc") / asString(pid);
119         const Pathname exeFile = pidDir / "exe";
120
121         auto res = in_our_root( exeFile );
122         if ( res > IGNORE )
123           return res == CONTAINER;
124
125         // if IGNORE was returned we need to continue testing all the files in /proc/<pid>/map_files until we hopefully
126         // find a still existing file. If all tests fail we will simply assume this pid is running on the HOST
127
128         // 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
129         std::unordered_set<std::string> tested;
130
131         // iterate over all the entries in /proc/<pid>/map_files
132         filesystem::dirForEach( pidDir / "map_files", [ this, &tested, &res ]( const Pathname & dir_r, const char *const & name_r  ){
133
134           // some helpers to make the code more self explanatory
135           constexpr bool contloop = true;
136           constexpr bool stoploop = false;
137
138           const Pathname entryName = dir_r / name_r;
139
140           // get the links target file and check if we alreadys know it, also if we can not read link information we skip the file
141           const Pathname linkTarget = filesystem::readlink( entryName );
142           if ( linkTarget.empty() || !tested.insert( linkTarget.asString() ).second ) return contloop;
143
144           // try to get file type
145           const auto mappedFileType = in_our_root( entryName );
146
147           // if we got something, remember the value and stop the loop
148           if ( mappedFileType > IGNORE ) {
149             res = mappedFileType;
150             return stoploop;
151           }
152           return contloop;
153         });
154
155         // if res is still IGNORE we did not find a explicit answer. So to be safe we assume its running on the host
156         if ( res == IGNORE )
157           return false; // can't tell for sure, lets assume host
158
159         return res == CONTAINER;
160       }
161
162       FilterRunsInContainer() {}
163     };
164
165
166     /** bsc#1099847: Check for lsof version < 4.90 which does not support '-K i'
167      * Just a quick check to allow code15 libzypp runnig in a code12 environment.
168      * bsc#1036304: '-K i' was backported to older lsof versions, indicated by
169      * lsof providing 'backported-option-Ki'.
170      */
171     bool lsofNoOptKi()
172     {
173       using target::rpm::librpmDb;
174       // RpmDb access is blocked while the Target is not initialized.
175       // Launching the Target just for this query would be an overkill.
176       struct TmpUnblock {
177         TmpUnblock()
178         : _wasBlocked( librpmDb::isBlocked() )
179         { if ( _wasBlocked ) librpmDb::unblockAccess(); }
180         ~TmpUnblock()
181         { if ( _wasBlocked ) librpmDb::blockAccess(); }
182       private:
183         bool _wasBlocked;
184       } tmpUnblock;
185
186       librpmDb::db_const_iterator it;
187       return( it.findPackage( "lsof" ) && it->tag_edition() < Edition("4.90") && !it->tag_provides().count( Capability("backported-option-Ki") ) );
188     }
189
190   } //namespace
191   /////////////////////////////////////////////////////////////////
192
193   class CheckAccessDeleted::Impl
194   {
195   public:
196     CheckAccessDeleted::Impl *clone() const;
197
198     bool addDataIf( const CacheEntry & cache_r, std::vector<std::string> *debMap = nullptr );
199     void addCacheIf( CacheEntry & cache_r, const std::string & line_r, std::vector<std::string> *debMap = nullptr );
200
201     std::map<pid_t,CacheEntry> filterInput( externalprogram::ExternalDataSource &source );
202     CheckAccessDeleted::size_type createProcInfo( const std::map<pid_t,CacheEntry> &in );
203
204     std::vector<CheckAccessDeleted::ProcInfo> _data;
205     bool _fromLsofFileMode = false; // Set if we currently process data from a debug file
206     bool _verbose = false;
207
208     std::map<pid_t,std::vector<std::string>> debugMap; //will contain all used lsof files after filtering
209     Pathname _debugFile;
210   };
211
212   CheckAccessDeleted::Impl *CheckAccessDeleted::Impl::clone() const
213   {
214     Impl *myClone = new Impl( *this );
215     return myClone;
216   }
217
218   /** Add \c cache to \c data if the process is accessing deleted files.
219    * \c pid string in \c cache is the proc line \c (pcuLR), \c files
220    * are already in place. Always clear the \c cache.files!
221   */
222   inline bool CheckAccessDeleted::Impl::addDataIf( const CacheEntry & cache_r, std::vector<std::string> *debMap )
223   {
224     const auto & filelist( cache_r.second );
225
226     if ( filelist.empty() )
227       return false;
228
229     // at least one file access so keep it:
230     _data.push_back( CheckAccessDeleted::ProcInfo() );
231     CheckAccessDeleted::ProcInfo & pinfo( _data.back() );
232     pinfo.files.insert( pinfo.files.begin(), filelist.begin(), filelist.end() );
233
234     const std::string & pline( cache_r.first );
235     std::string commandname;    // pinfo.command if still needed...
236     std::ostringstream pLineStr; //rewrite the first line in debug cache
237     for_( ch, pline.begin(), pline.end() )
238     {
239       switch ( *ch )
240       {
241         case 'p':
242           pinfo.pid = &*(ch+1);
243           if ( debMap )
244             pLineStr <<&*(ch)<<'\0';
245           break;
246         case 'R':
247           pinfo.ppid = &*(ch+1);
248           if ( debMap )
249             pLineStr <<&*(ch)<<'\0';
250           break;
251         case 'u':
252           pinfo.puid = &*(ch+1);
253           if ( debMap )
254             pLineStr <<&*(ch)<<'\0';
255           break;
256         case 'L':
257           pinfo.login = &*(ch+1);
258           if ( debMap )
259             pLineStr <<&*(ch)<<'\0';
260           break;
261         case 'c':
262           if ( pinfo.command.empty() ) {
263             commandname = &*(ch+1);
264             // the lsof command name might be truncated, so we prefer /proc/<pid>/exe
265             if (!_fromLsofFileMode)
266               pinfo.command = filesystem::readlink( Pathname("/proc")/pinfo.pid/"exe" ).basename();
267             if ( pinfo.command.empty() )
268               pinfo.command = std::move(commandname);
269             if ( debMap )
270               pLineStr <<'c'<<pinfo.command<<'\0';
271           }
272           break;
273       }
274       if ( *ch == '\n' ) break;         // end of data
275       do { ++ch; } while ( *ch != '\0' );       // skip to next field
276     }
277
278     //replace the data in the debug cache as well
279     if ( debMap ) {
280       pLineStr<<endl;
281       debMap->front() = pLineStr.str();
282     }
283
284     //entry was added
285     return true;
286   }
287
288
289   /** Add file to cache if it refers to a deleted executable or library file:
290    * - Either the link count \c(k) is \c 0, or no link cout is present.
291    * - The type \c (t) is set to \c REG or \c DEL
292    * - The filedescriptor \c (f) is set to \c txt, \c mem or \c DEL
293   */
294   inline void CheckAccessDeleted::Impl::addCacheIf( CacheEntry & cache_r, const std::string & line_r, std::vector<std::string> *debMap )
295   {
296     const char * f = 0;
297     const char * t = 0;
298     const char * n = 0;
299
300     for_( ch, line_r.c_str(), ch+line_r.size() )
301     {
302       switch ( *ch )
303       {
304         case 'k':
305           if ( *(ch+1) != '0' ) // skip non-zero link counts
306             return;
307           break;
308         case 'f':
309           f = ch+1;
310           break;
311         case 't':
312           t = ch+1;
313           break;
314         case 'n':
315           n = ch+1;
316           break;
317       }
318       if ( *ch == '\n' ) break;         // end of data
319       do { ++ch; } while ( *ch != '\0' );       // skip to next field
320     }
321
322     if ( !t || !f || !n )
323       return;   // wrong filedescriptor/type/name
324
325     if ( !(    ( *t == 'R' && *(t+1) == 'E' && *(t+2) == 'G' && *(t+3) == '\0' )
326             || ( *t == 'D' && *(t+1) == 'E' && *(t+2) == 'L' && *(t+3) == '\0' ) ) )
327       return;   // wrong type
328
329     if ( !(    ( *f == 'm' && *(f+1) == 'e' && *(f+2) == 'm' && *(f+3) == '\0' )
330             || ( *f == 't' && *(f+1) == 'x' && *(f+2) == 't' && *(f+3) == '\0' )
331             || ( *f == 'D' && *(f+1) == 'E' && *(f+2) == 'L' && *(f+3) == '\0' )
332             || ( *f == 'l' && *(f+1) == 't' && *(f+2) == 'x' && *(f+3) == '\0' ) ) )
333       return;   // wrong filedescriptor type
334
335     if ( str::contains( n, "(stat: Permission denied)" ) )
336       return;   // Avoid reporting false positive due to insufficient permission.
337
338     if ( ! _verbose )
339     {
340       if ( ! ( str::contains( n, "/lib" ) || str::contains( n, "bin/" ) ) )
341         return; // Try to avoid reporting false positive unless verbose.
342     }
343
344     if ( *f == 'm' || *f == 'D' )       // skip some wellknown nonlibrary memorymapped files
345     {
346       static const char * black[] = {
347           "/SYSV"
348         , "/var/"
349         , "/dev/"
350         , "/tmp/"
351         , "/proc/"
352         , "/memfd:"
353       };
354       for_( it, arrayBegin( black ), arrayEnd( black ) )
355       {
356         if ( str::hasPrefix( n, *it ) )
357           return;
358       }
359     }
360     // Add if no duplicate
361     if ( debMap && cache_r.second.find(n) == cache_r.second.end() ) {
362       debMap->push_back(line_r);
363     }
364     cache_r.second.insert( n );
365   }
366
367   CheckAccessDeleted::CheckAccessDeleted( bool doCheck_r )
368     : _pimpl(new Impl)
369   {
370     if ( doCheck_r ) check();
371   }
372
373   CheckAccessDeleted::size_type CheckAccessDeleted::check( const Pathname &lsofOutput_r, bool verbose_r )
374   {
375     _pimpl->_verbose = verbose_r;
376     _pimpl->_fromLsofFileMode = true;
377
378     FILE *inFile = fopen( lsofOutput_r.c_str(), "r" );
379     if ( !inFile ) {
380       ZYPP_THROW( Exception(  str::Format("Opening input file %1% failed.") % lsofOutput_r.c_str() ) );
381     }
382
383     //inFile is closed by ExternalDataSource
384     externalprogram::ExternalDataSource inSource( inFile, nullptr );
385     auto cache = _pimpl->filterInput( inSource );
386     return _pimpl->createProcInfo( cache );
387   }
388
389   std::map<pid_t,CacheEntry> CheckAccessDeleted::Impl::filterInput( externalprogram::ExternalDataSource &source )
390   {
391     // cachemap: PID => (deleted files)
392     // NOTE: omit PIDs running in a (lxc/docker) container
393     std::map<pid_t,CacheEntry> cachemap;
394
395     bool debugEnabled = !_debugFile.empty();
396
397     pid_t cachepid = 0;
398     FilterRunsInContainer runsInLXC;
399     for( std::string line = source.receiveLine(); ! line.empty(); line = source.receiveLine() )
400     {
401       // NOTE: line contains '\0' separeated fields!
402       if ( line[0] == 'p' )
403       {
404         str::strtonum( line.c_str()+1, cachepid );      // line is "p<PID>\0...."
405         if ( _fromLsofFileMode || !runsInLXC( cachepid ) ) {
406           if ( debugEnabled ) {
407             auto &pidMad = debugMap[cachepid];
408             if ( pidMad.empty() )
409               debugMap[cachepid].push_back( line );
410             else
411               debugMap[cachepid].front() = line;
412           }
413           cachemap[cachepid].first.swap( line );
414         } else {
415           cachepid = 0; // ignore this pid
416         }
417       }
418       else if ( cachepid )
419       {
420         auto &dbgMap = debugMap[cachepid];
421         addCacheIf( cachemap[cachepid], line, debugEnabled ? &dbgMap : nullptr);
422       }
423     }
424     return cachemap;
425   }
426
427   CheckAccessDeleted::size_type CheckAccessDeleted::check( bool verbose_r  )
428   {
429     static const char* argv[] = { "lsof", "-n", "-FpcuLRftkn0", "-K", "i", NULL };
430     if ( lsofNoOptKi() )
431       argv[3] = NULL;
432
433     _pimpl->_verbose = verbose_r;
434     _pimpl->_fromLsofFileMode = false;
435
436     ExternalProgram prog( argv, ExternalProgram::Discard_Stderr );
437     std::map<pid_t,CacheEntry> cachemap = _pimpl->filterInput( prog );
438
439     int ret = prog.close();
440     if ( ret != 0 )
441     {
442       if ( ret == 129 )
443       {
444         ZYPP_THROW( Exception(_("Please install package 'lsof' first.") ) );
445       }
446       Exception err( str::Format("Executing 'lsof' failed (%1%).") % ret );
447       err.remember( prog.execError() );
448       ZYPP_THROW( err );
449     }
450
451     return _pimpl->createProcInfo( cachemap );
452   }
453
454   CheckAccessDeleted::size_type CheckAccessDeleted::Impl::createProcInfo(const std::map<pid_t,CacheEntry> &in)
455   {
456     std::ofstream debugFileOut;
457     bool debugEnabled = false;
458     if ( !_debugFile.empty() ) {
459       debugFileOut.open( _debugFile.c_str() );
460       debugEnabled =  debugFileOut.is_open();
461
462       if ( !debugEnabled ) {
463         ERR<<"Unable to open debug file: "<<_debugFile<<endl;
464       }
465     }
466
467     _data.clear();
468     for ( const auto &cached : in )
469     {
470       if (!debugEnabled)
471         addDataIf( cached.second);
472       else {
473         std::vector<std::string> *mapPtr = nullptr;
474
475         auto dbgInfo = debugMap.find(cached.first);
476         if ( dbgInfo != debugMap.end() )
477           mapPtr = &(dbgInfo->second);
478
479         if( !addDataIf( cached.second, mapPtr ) )
480           continue;
481
482         for ( const std::string &dbgLine: dbgInfo->second ) {
483           debugFileOut.write( dbgLine.c_str(), dbgLine.length() );
484         }
485       }
486     }
487     return _data.size();
488   }
489
490   bool CheckAccessDeleted::empty() const
491   {
492     return _pimpl->_data.empty();
493   }
494
495   CheckAccessDeleted::size_type CheckAccessDeleted::size() const
496   {
497     return _pimpl->_data.size();
498   }
499
500   CheckAccessDeleted::const_iterator CheckAccessDeleted::begin() const
501   {
502     return _pimpl->_data.begin();
503   }
504
505   CheckAccessDeleted::const_iterator CheckAccessDeleted::end() const
506   {
507     return _pimpl->_data.end();
508   }
509
510   void CheckAccessDeleted::setDebugOutputFile(const Pathname &filename_r)
511   {
512     _pimpl->_debugFile = filename_r;
513   }
514
515   std::string CheckAccessDeleted::findService( pid_t pid_r )
516   {
517     ProcInfo p;
518     p.pid = str::numstring( pid_r );
519     return p.service();
520   }
521
522   std::string CheckAccessDeleted::ProcInfo::service() const
523   {
524     static const str::regex rx( "[0-9]+:name=systemd:/system.slice/(.*/)?(.*).service$" );
525     str::smatch what;
526     std::string ret;
527     iostr::simpleParseFile( InputStream( Pathname("/proc")/pid/"cgroup" ),
528                             [&]( int num_r, std::string line_r )->bool
529                             {
530                               if ( str::regex_match( line_r, what, rx ) )
531                               {
532                                 ret = what[2];
533                                 return false;   // stop after match
534                               }
535                               return true;
536                             } );
537     return ret;
538   }
539
540   /******************************************************************
541   **
542   **    FUNCTION NAME : operator<<
543   **    FUNCTION TYPE : std::ostream &
544   */
545   std::ostream & operator<<( std::ostream & str, const CheckAccessDeleted & obj )
546   {
547     return dumpRange( str << "CheckAccessDeleted ",
548                       obj.begin(),
549                       obj.end() );
550   }
551
552    /******************************************************************
553   **
554   **    FUNCTION NAME : operator<<
555   **    FUNCTION TYPE : std::ostream &
556   */
557   std::ostream & operator<<( std::ostream & str, const CheckAccessDeleted::ProcInfo & obj )
558   {
559     if ( obj.pid.empty() )
560       return str << "<NoProc>";
561
562     return dumpRangeLine( str << obj.command
563                               << '<' << obj.pid
564                               << '|' << obj.ppid
565                               << '|' << obj.puid
566                               << '|' << obj.login
567                               << '>',
568                           obj.files.begin(),
569                           obj.files.end() );
570   }
571
572  /////////////////////////////////////////////////////////////////
573 } // namespace zypp
574 ///////////////////////////////////////////////////////////////////