Imported Upstream version 17.25.4
[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/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>
22
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>
29
30 #include <zypp/misc/CheckAccessDeleted.h>
31
32 using std::endl;
33
34 #undef ZYPP_BASE_LOGGER_LOGGROUP
35 #define ZYPP_BASE_LOGGER_LOGGROUP "zypp::misc"
36
37 ///////////////////////////////////////////////////////////////////
38 namespace zypp
39 { /////////////////////////////////////////////////////////////////
40
41   ///////////////////////////////////////////////////////////////////
42   namespace
43   { /////////////////////////////////////////////////////////////////
44     //
45     // lsof output lines are a sequence of NUL terminated fields,
46     // where the 1st char determines the fields type.
47     //
48     // (pcuL) pid command userid loginname
49     // (ftkn).filedescriptor type linkcount filename
50     //
51     /////////////////////////////////////////////////////////////////
52
53     /** lsof output line + files extracted so far for this PID */
54     typedef std::pair<std::string,std::unordered_set<std::string>> CacheEntry;
55
56     /////////////////////////////////////////////////////////////////
57     /// \class FilterRunsInContainer
58     /// \brief Functor guessing whether \a PID is running in a container.
59     ///
60     /// Use /proc to guess if a process is running in a container
61     /////////////////////////////////////////////////////////////////
62     struct FilterRunsInContainer
63     {
64     private:
65
66       enum Type {
67         IGNORE,
68         HOST,
69         CONTAINER
70       };
71
72       /*!
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.
76        */
77       Type in_our_root( const Pathname &path ) const {
78
79         const PathInfo procInfoStat( path );
80
81         // if we can not stat the file continue to the next one
82         if ( procInfoStat.error() ) return IGNORE;
83
84         // if the file was unlinked ignore it
85         if ( procInfoStat.nlink() == 0 )
86           return IGNORE;
87
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;
91
92         // get stat info for the target file
93         const PathInfo linkStat( linkTarget );
94
95         // Non-existent path means it's not reachable by us.
96         if ( !linkStat.isExist() )
97           return CONTAINER;
98
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())
101           return CONTAINER;
102
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() )
105           return CONTAINER;
106
107         // assume HOST if all tests fail
108         return HOST;
109       }
110
111     public:
112
113       /*!
114        * Iterates over the /proc contents for the given pid
115        */
116       bool operator()( const pid_t pid ) const {
117
118         // first check the exe file
119         const Pathname pidDir  = Pathname("/proc") / asString(pid);
120         const Pathname exeFile = pidDir / "exe";
121
122         auto res = in_our_root( exeFile );
123         if ( res > IGNORE )
124           return res == CONTAINER;
125
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
128
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;
131
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  ){
134
135           // some helpers to make the code more self explanatory
136           constexpr bool contloop = true;
137           constexpr bool stoploop = false;
138
139           const Pathname entryName = dir_r / name_r;
140
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;
144
145           // try to get file type
146           const auto mappedFileType = in_our_root( entryName );
147
148           // if we got something, remember the value and stop the loop
149           if ( mappedFileType > IGNORE ) {
150             res = mappedFileType;
151             return stoploop;
152           }
153           return contloop;
154         });
155
156         // if res is still IGNORE we did not find a explicit answer. So to be safe we assume its running on the host
157         if ( res == IGNORE )
158           return false; // can't tell for sure, lets assume host
159
160         return res == CONTAINER;
161       }
162
163       FilterRunsInContainer() {}
164     };
165
166
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'.
171      */
172     bool lsofNoOptKi()
173     {
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.
177       struct TmpUnblock {
178         TmpUnblock()
179         : _wasBlocked( librpmDb::isBlocked() )
180         { if ( _wasBlocked ) librpmDb::unblockAccess(); }
181         ~TmpUnblock()
182         { if ( _wasBlocked ) librpmDb::blockAccess(); }
183       private:
184         bool _wasBlocked;
185       } tmpUnblock;
186
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") ) );
189     }
190
191   } //namespace
192   /////////////////////////////////////////////////////////////////
193
194   class CheckAccessDeleted::Impl
195   {
196   public:
197     CheckAccessDeleted::Impl *clone() const;
198
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 );
201
202     std::map<pid_t,CacheEntry> filterInput( externalprogram::ExternalDataSource &source );
203     CheckAccessDeleted::size_type createProcInfo( const std::map<pid_t,CacheEntry> &in );
204
205     std::vector<CheckAccessDeleted::ProcInfo> _data;
206     bool _fromLsofFileMode = false; // Set if we currently process data from a debug file
207     bool _verbose = false;
208
209     std::map<pid_t,std::vector<std::string>> debugMap; //will contain all used lsof files after filtering
210     Pathname _debugFile;
211   };
212
213   CheckAccessDeleted::Impl *CheckAccessDeleted::Impl::clone() const
214   {
215     Impl *myClone = new Impl( *this );
216     return myClone;
217   }
218
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!
222   */
223   inline bool CheckAccessDeleted::Impl::addDataIf( const CacheEntry & cache_r, std::vector<std::string> *debMap )
224   {
225     const auto & filelist( cache_r.second );
226
227     if ( filelist.empty() )
228       return false;
229
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() );
234
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() )
239     {
240       switch ( *ch )
241       {
242         case 'p':
243           pinfo.pid = &*(ch+1);
244           if ( debMap )
245             pLineStr <<&*(ch)<<'\0';
246           break;
247         case 'R':
248           pinfo.ppid = &*(ch+1);
249           if ( debMap )
250             pLineStr <<&*(ch)<<'\0';
251           break;
252         case 'u':
253           pinfo.puid = &*(ch+1);
254           if ( debMap )
255             pLineStr <<&*(ch)<<'\0';
256           break;
257         case 'L':
258           pinfo.login = &*(ch+1);
259           if ( debMap )
260             pLineStr <<&*(ch)<<'\0';
261           break;
262         case 'c':
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);
270             if ( debMap )
271               pLineStr <<'c'<<pinfo.command<<'\0';
272           }
273           break;
274       }
275       if ( *ch == '\n' ) break;         // end of data
276       do { ++ch; } while ( *ch != '\0' );       // skip to next field
277     }
278
279     //replace the data in the debug cache as well
280     if ( debMap ) {
281       pLineStr<<endl;
282       debMap->front() = pLineStr.str();
283     }
284
285     //entry was added
286     return true;
287   }
288
289
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
294   */
295   inline void CheckAccessDeleted::Impl::addCacheIf( CacheEntry & cache_r, const std::string & line_r, std::vector<std::string> *debMap )
296   {
297     const char * f = 0;
298     const char * t = 0;
299     const char * n = 0;
300
301     for_( ch, line_r.c_str(), ch+line_r.size() )
302     {
303       switch ( *ch )
304       {
305         case 'k':
306           if ( *(ch+1) != '0' ) // skip non-zero link counts
307             return;
308           break;
309         case 'f':
310           f = ch+1;
311           break;
312         case 't':
313           t = ch+1;
314           break;
315         case 'n':
316           n = ch+1;
317           break;
318       }
319       if ( *ch == '\n' ) break;         // end of data
320       do { ++ch; } while ( *ch != '\0' );       // skip to next field
321     }
322
323     if ( !t || !f || !n )
324       return;   // wrong filedescriptor/type/name
325
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
329
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
335
336     if ( str::contains( n, "(stat: Permission denied)" ) )
337       return;   // Avoid reporting false positive due to insufficient permission.
338
339     if ( ! _verbose )
340     {
341       if ( ! ( str::contains( n, "/lib" ) || str::contains( n, "bin/" ) ) )
342         return; // Try to avoid reporting false positive unless verbose.
343     }
344
345     if ( *f == 'm' || *f == 'D' )       // skip some wellknown nonlibrary memorymapped files
346     {
347       static const char * black[] = {
348           "/SYSV"
349         , "/var/"
350         , "/dev/"
351         , "/tmp/"
352         , "/proc/"
353         , "/memfd:"
354       };
355       for_( it, arrayBegin( black ), arrayEnd( black ) )
356       {
357         if ( str::hasPrefix( n, *it ) )
358           return;
359       }
360     }
361     // Add if no duplicate
362     if ( debMap && cache_r.second.find(n) == cache_r.second.end() ) {
363       debMap->push_back(line_r);
364     }
365     cache_r.second.insert( n );
366   }
367
368   CheckAccessDeleted::CheckAccessDeleted( bool doCheck_r )
369     : _pimpl(new Impl)
370   {
371     if ( doCheck_r ) check();
372   }
373
374   CheckAccessDeleted::size_type CheckAccessDeleted::check( const Pathname &lsofOutput_r, bool verbose_r )
375   {
376     _pimpl->_verbose = verbose_r;
377     _pimpl->_fromLsofFileMode = true;
378
379     FILE *inFile = fopen( lsofOutput_r.c_str(), "r" );
380     if ( !inFile ) {
381       ZYPP_THROW( Exception(  str::Format("Opening input file %1% failed.") % lsofOutput_r.c_str() ) );
382     }
383
384     //inFile is closed by ExternalDataSource
385     externalprogram::ExternalDataSource inSource( inFile, nullptr );
386     auto cache = _pimpl->filterInput( inSource );
387     return _pimpl->createProcInfo( cache );
388   }
389
390   std::map<pid_t,CacheEntry> CheckAccessDeleted::Impl::filterInput( externalprogram::ExternalDataSource &source )
391   {
392     // cachemap: PID => (deleted files)
393     // NOTE: omit PIDs running in a (lxc/docker) container
394     std::map<pid_t,CacheEntry> cachemap;
395
396     bool debugEnabled = !_debugFile.empty();
397
398     pid_t cachepid = 0;
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  ) )
403     {
404       // NOTE: line contains '\0' separeated fields!
405       if ( line[0] == 'p' )
406       {
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 );
413             else
414               debugMap[cachepid].front() = line;
415           }
416           cachemap[cachepid].first.swap( line );
417         } else {
418           cachepid = 0; // ignore this pid
419         }
420       }
421       else if ( cachepid )
422       {
423         auto &dbgMap = debugMap[cachepid];
424         addCacheIf( cachemap[cachepid], line, debugEnabled ? &dbgMap : nullptr);
425       }
426     }
427     return cachemap;
428   }
429
430   CheckAccessDeleted::size_type CheckAccessDeleted::check( bool verbose_r  )
431   {
432     static const char* argv[] = { "lsof", "-n", "-FpcuLRftkn0", "-K", "i", NULL };
433     if ( lsofNoOptKi() )
434       argv[3] = NULL;
435
436     _pimpl->_verbose = verbose_r;
437     _pimpl->_fromLsofFileMode = false;
438
439     ExternalProgram prog( argv, ExternalProgram::Discard_Stderr );
440     std::map<pid_t,CacheEntry> cachemap;
441
442     try {
443       cachemap = _pimpl->filterInput( prog );
444     } catch ( const io::TimeoutException &e ) {
445       ZYPP_CAUGHT( e );
446       prog.close();
447       ZYPP_THROW ( Exception( "Reading data from 'lsof' timed out.") );
448     }
449
450     int ret = prog.close();
451     if ( ret != 0 )
452     {
453       if ( ret == 129 )
454       {
455         ZYPP_THROW( Exception(_("Please install package 'lsof' first.") ) );
456       }
457       Exception err( str::Format("Executing 'lsof' failed (%1%).") % ret );
458       err.remember( prog.execError() );
459       ZYPP_THROW( err );
460     }
461
462     return _pimpl->createProcInfo( cachemap );
463   }
464
465   CheckAccessDeleted::size_type CheckAccessDeleted::Impl::createProcInfo(const std::map<pid_t,CacheEntry> &in)
466   {
467     std::ofstream debugFileOut;
468     bool debugEnabled = false;
469     if ( !_debugFile.empty() ) {
470       debugFileOut.open( _debugFile.c_str() );
471       debugEnabled =  debugFileOut.is_open();
472
473       if ( !debugEnabled ) {
474         ERR<<"Unable to open debug file: "<<_debugFile<<endl;
475       }
476     }
477
478     _data.clear();
479     for ( const auto &cached : in )
480     {
481       if (!debugEnabled)
482         addDataIf( cached.second);
483       else {
484         std::vector<std::string> *mapPtr = nullptr;
485
486         auto dbgInfo = debugMap.find(cached.first);
487         if ( dbgInfo != debugMap.end() )
488           mapPtr = &(dbgInfo->second);
489
490         if( !addDataIf( cached.second, mapPtr ) )
491           continue;
492
493         for ( const std::string &dbgLine: dbgInfo->second ) {
494           debugFileOut.write( dbgLine.c_str(), dbgLine.length() );
495         }
496       }
497     }
498     return _data.size();
499   }
500
501   bool CheckAccessDeleted::empty() const
502   {
503     return _pimpl->_data.empty();
504   }
505
506   CheckAccessDeleted::size_type CheckAccessDeleted::size() const
507   {
508     return _pimpl->_data.size();
509   }
510
511   CheckAccessDeleted::const_iterator CheckAccessDeleted::begin() const
512   {
513     return _pimpl->_data.begin();
514   }
515
516   CheckAccessDeleted::const_iterator CheckAccessDeleted::end() const
517   {
518     return _pimpl->_data.end();
519   }
520
521   void CheckAccessDeleted::setDebugOutputFile(const Pathname &filename_r)
522   {
523     _pimpl->_debugFile = filename_r;
524   }
525
526   std::string CheckAccessDeleted::findService( pid_t pid_r )
527   {
528     ProcInfo p;
529     p.pid = str::numstring( pid_r );
530     return p.service();
531   }
532
533   std::string CheckAccessDeleted::ProcInfo::service() const
534   {
535     static const str::regex rx( "[0-9]+:name=systemd:/system.slice/(.*/)?(.*).service$" );
536     str::smatch what;
537     std::string ret;
538     iostr::simpleParseFile( InputStream( Pathname("/proc")/pid/"cgroup" ),
539                             [&]( int num_r, std::string line_r )->bool
540                             {
541                               if ( str::regex_match( line_r, what, rx ) )
542                               {
543                                 ret = what[2];
544                                 return false;   // stop after match
545                               }
546                               return true;
547                             } );
548     return ret;
549   }
550
551   /******************************************************************
552   **
553   **    FUNCTION NAME : operator<<
554   **    FUNCTION TYPE : std::ostream &
555   */
556   std::ostream & operator<<( std::ostream & str, const CheckAccessDeleted & obj )
557   {
558     return dumpRange( str << "CheckAccessDeleted ",
559                       obj.begin(),
560                       obj.end() );
561   }
562
563    /******************************************************************
564   **
565   **    FUNCTION NAME : operator<<
566   **    FUNCTION TYPE : std::ostream &
567   */
568   std::ostream & operator<<( std::ostream & str, const CheckAccessDeleted::ProcInfo & obj )
569   {
570     if ( obj.pid.empty() )
571       return str << "<NoProc>";
572
573     return dumpRangeLine( str << obj.command
574                               << '<' << obj.pid
575                               << '|' << obj.ppid
576                               << '|' << obj.puid
577                               << '|' << obj.login
578                               << '>',
579                           obj.files.begin(),
580                           obj.files.end() );
581   }
582
583  /////////////////////////////////////////////////////////////////
584 } // namespace zypp
585 ///////////////////////////////////////////////////////////////////