2020-04-30 21:33:58 +02:00
# FRR ELF xref extractor
#
# Copyright (C) 2020 David Lamparter for NetDEF, Inc.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; see the file COPYING; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import sys
import os
import struct
import re
import traceback
2022-10-04 18:01:08 +02:00
json_dump_args = { }
try :
import ujson as json
json_dump_args [ " escape_forward_slashes " ] = False
except ImportError :
import json
2020-04-30 21:33:58 +02:00
import argparse
from clippy . uidhash import uidhash
from clippy . elf import *
2022-10-04 13:30:04 +02:00
from clippy import frr_top_src , CmdAttr
2020-04-30 21:33:58 +02:00
from tiabwarfo import FieldApplicator
2022-10-04 18:44:36 +02:00
from xref2vtysh import CommandEntry
2020-04-30 21:33:58 +02:00
try :
with open ( os . path . join ( frr_top_src , ' python ' , ' xrefstructs.json ' ) , ' r ' ) as fd :
xrefstructs = json . load ( fd )
except FileNotFoundError :
sys . stderr . write ( '''
The " xrefstructs.json " file ( created by running tiabwarfo . py with the pahole
tool available ) could not be found . It should be included with the sources .
''' )
sys . exit ( 1 )
# constants, need to be kept in sync manually...
XREFT_THREADSCHED = 0x100
XREFT_LOGMSG = 0x200
XREFT_DEFUN = 0x300
XREFT_INSTALL_ELEMENT = 0x301
# LOG_*
priovals = { }
prios = [ ' 0 ' , ' 1 ' , ' 2 ' , ' E ' , ' W ' , ' N ' , ' I ' , ' D ' ]
class XrelfoJson ( object ) :
def dump ( self ) :
pass
def check ( self , wopt ) :
yield from [ ]
def to_dict ( self , refs ) :
pass
class Xref ( ELFDissectStruct , XrelfoJson ) :
struct = ' xref '
fieldrename = { ' type ' : ' typ ' }
containers = { }
def __init__ ( self , * args , * * kwargs ) :
super ( ) . __init__ ( * args , * * kwargs )
self . _container = None
if self . xrefdata :
self . xrefdata . ref_from ( self , self . typ )
def container ( self ) :
if self . _container is None :
if self . typ in self . containers :
self . _container = self . container_of ( self . containers [ self . typ ] , ' xref ' )
return self . _container
def check ( self , * args , * * kwargs ) :
if self . _container :
yield from self . _container . check ( * args , * * kwargs )
class Xrefdata ( ELFDissectStruct ) :
struct = ' xrefdata '
# uid is all zeroes in the data loaded from ELF
fieldrename = { ' uid ' : ' _uid ' }
def ref_from ( self , xref , typ ) :
self . xref = xref
@property
def uid ( self ) :
if self . hashstr is None :
return None
return uidhash ( self . xref . file , self . hashstr , self . hashu32_0 , self . hashu32_1 )
class XrefPtr ( ELFDissectStruct ) :
fields = [
( ' xref ' , ' P ' , Xref ) ,
]
class XrefThreadSched ( ELFDissectStruct , XrelfoJson ) :
struct = ' xref_threadsched '
Xref . containers [ XREFT_THREADSCHED ] = XrefThreadSched
class XrefLogmsg ( ELFDissectStruct , XrelfoJson ) :
struct = ' xref_logmsg '
def _warn_fmt ( self , text ) :
2021-02-14 00:53:27 +01:00
lines = text . split ( ' \n ' )
yield ( ( self . xref . file , self . xref . line ) , ' %s : %d : %s (in %s ()) %s \n ' % ( self . xref . file , self . xref . line , lines [ 0 ] , self . xref . func , ' ' . join ( [ ' \n ' + l for l in lines [ 1 : ] ] ) ) )
2020-04-30 21:33:58 +02:00
2021-02-14 00:53:27 +01:00
fmt_regexes = [
2020-04-30 21:33:58 +02:00
( re . compile ( r ' ([ \ n \ t]+) ' ) , ' error: log message contains tab or newline ' ) ,
# (re.compile(r'^(\s+)'), 'warning: log message starts with whitespace'),
( re . compile ( r ' ^((?:warn(?:ing)?|error): \ s*) ' , re . I ) , ' warning: log message starts with severity ' ) ,
]
2021-02-14 00:53:27 +01:00
arg_regexes = [
# the (?<![\?:] ) avoids warning for x ? inet_ntop(...) : "(bla)"
( re . compile ( r ' ((?<![ \ ?:] )inet_ntop \ s* \ ( \ s*(?:[AP]F_INET|2) \ s*,) ' ) , ' cleanup: replace inet_ntop(AF_INET, ...) with % pI4 ' , lambda s : True ) ,
( re . compile ( r ' ((?<![ \ ?:] )inet_ntop \ s* \ ( \ s*(?:[AP]F_INET6|10) \ s*,) ' ) , ' cleanup: replace inet_ntop(AF_INET6, ...) with % pI6 ' , lambda s : True ) ,
( re . compile ( r ' ((?<![ \ ?:] )inet_ntoa) ' ) , ' cleanup: replace inet_ntoa(...) with % pI4 ' , lambda s : True ) ,
( re . compile ( r ' ((?<![ \ ?:] )ipaddr2str) ' ) , ' cleanup: replace ipaddr2str(...) with % pIA ' , lambda s : True ) ,
( re . compile ( r ' ((?<![ \ ?:] )prefix2str) ' ) , ' cleanup: replace prefix2str(...) with % pFX ' , lambda s : True ) ,
( re . compile ( r ' ((?<![ \ ?:] )prefix_mac2str) ' ) , ' cleanup: replace prefix_mac2str(...) with % pEA ' , lambda s : True ) ,
( re . compile ( r ' ((?<![ \ ?:] )sockunion2str) ' ) , ' cleanup: replace sockunion2str(...) with % pSU ' , lambda s : True ) ,
# (re.compile(r'^(\s*__(?:func|FUNCTION|PRETTY_FUNCTION)__\s*)'), 'error: debug message starts with __func__', lambda s: (s.priority & 7 == 7) ),
]
2020-04-30 21:33:58 +02:00
def check ( self , wopt ) :
2021-02-14 00:53:27 +01:00
def fmt_msg ( rex , itext ) :
if sys . stderr . isatty ( ) :
items = rex . split ( itext )
out = [ ]
for i , text in enumerate ( items ) :
if ( i % 2 ) == 1 :
out . append ( ' \033 [41;37;1m %s \033 [m ' % repr ( text ) [ 1 : - 1 ] )
else :
out . append ( repr ( text ) [ 1 : - 1 ] )
excerpt = ' ' . join ( out )
else :
excerpt = repr ( itext ) [ 1 : - 1 ]
return excerpt
2020-04-30 21:33:58 +02:00
if wopt . Wlog_format :
2021-02-14 00:53:27 +01:00
for rex , msg in self . fmt_regexes :
2020-04-30 21:33:58 +02:00
if not rex . search ( self . fmtstring ) :
continue
2021-02-14 00:53:27 +01:00
excerpt = fmt_msg ( rex , self . fmtstring )
yield from self . _warn_fmt ( ' %s : " %s " ' % ( msg , excerpt ) )
2020-04-30 21:33:58 +02:00
2021-02-14 00:53:27 +01:00
if wopt . Wlog_args :
for rex , msg , cond in self . arg_regexes :
if not cond ( self ) :
continue
if not rex . search ( self . args ) :
continue
2020-04-30 21:33:58 +02:00
2021-02-14 00:53:27 +01:00
excerpt = fmt_msg ( rex , self . args )
yield from self . _warn_fmt ( ' %s : \n \t " %s " , \n \t %s ' % ( msg , repr ( self . fmtstring ) [ 1 : - 1 ] , excerpt ) )
2020-04-30 21:33:58 +02:00
def dump ( self ) :
print ( ' %-60s %s %s %-25s [EC %d ] %s ' % (
' %s : %d %s () ' % ( self . xref . file , self . xref . line , self . xref . func ) ,
prios [ self . priority & 7 ] ,
priovals . get ( self . priority & 0x30 , ' ' ) ,
self . xref . xrefdata . uid , self . ec , self . fmtstring ) )
def to_dict ( self , xrelfo ) :
jsobj = dict ( [ ( i , getattr ( self . xref , i ) ) for i in [ ' file ' , ' line ' , ' func ' ] ] )
if self . ec != 0 :
jsobj [ ' ec ' ] = self . ec
jsobj [ ' fmtstring ' ] = self . fmtstring
2021-02-14 00:53:27 +01:00
jsobj [ ' args ' ] = self . args
2020-04-30 21:33:58 +02:00
jsobj [ ' priority ' ] = self . priority & 7
jsobj [ ' type ' ] = ' logmsg '
jsobj [ ' binary ' ] = self . _elfsect . _elfwrap . orig_filename
if self . priority & 0x10 :
jsobj . setdefault ( ' flags ' , [ ] ) . append ( ' errno ' )
if self . priority & 0x20 :
jsobj . setdefault ( ' flags ' , [ ] ) . append ( ' getaddrinfo ' )
xrelfo [ ' refs ' ] . setdefault ( self . xref . xrefdata . uid , [ ] ) . append ( jsobj )
Xref . containers [ XREFT_LOGMSG ] = XrefLogmsg
class CmdElement ( ELFDissectStruct , XrelfoJson ) :
struct = ' cmd_element '
def __init__ ( self , * args , * * kwargs ) :
super ( ) . __init__ ( * args , * * kwargs )
def to_dict ( self , xrelfo ) :
jsobj = xrelfo [ ' cli ' ] . setdefault ( self . name , { } ) . setdefault ( self . _elfsect . _elfwrap . orig_filename , { } )
jsobj . update ( {
' string ' : self . string ,
' doc ' : self . doc ,
} )
2022-10-04 13:30:04 +02:00
if self . attr :
jsobj [ ' attr ' ] = attr = self . attr
for attrname in CmdAttr . __members__ :
val = CmdAttr [ attrname ]
if attr & val :
jsobj . setdefault ( ' attrs ' , [ ] ) . append ( attrname . lower ( ) )
attr & = ~ val
2020-04-30 21:33:58 +02:00
jsobj [ ' defun ' ] = dict ( [ ( i , getattr ( self . xref , i ) ) for i in [ ' file ' , ' line ' , ' func ' ] ] )
Xref . containers [ XREFT_DEFUN ] = CmdElement
class XrefInstallElement ( ELFDissectStruct , XrelfoJson ) :
struct = ' xref_install_element '
def to_dict ( self , xrelfo ) :
jsobj = xrelfo [ ' cli ' ] . setdefault ( self . cmd_element . name , { } ) . setdefault ( self . _elfsect . _elfwrap . orig_filename , { } )
nodes = jsobj . setdefault ( ' nodes ' , [ ] )
nodes . append ( {
' node ' : self . node_type ,
' install ' : dict ( [ ( i , getattr ( self . xref , i ) ) for i in [ ' file ' , ' line ' , ' func ' ] ] ) ,
} )
Xref . containers [ XREFT_INSTALL_ELEMENT ] = XrefInstallElement
# shove in field defs
fieldapply = FieldApplicator ( xrefstructs )
fieldapply . add ( Xref )
fieldapply . add ( Xrefdata )
fieldapply . add ( XrefLogmsg )
fieldapply . add ( XrefThreadSched )
fieldapply . add ( CmdElement )
fieldapply . add ( XrefInstallElement )
fieldapply ( )
class Xrelfo ( dict ) :
def __init__ ( self ) :
super ( ) . __init__ ( {
' refs ' : { } ,
' cli ' : { } ,
} )
self . _xrefs = [ ]
def load_file ( self , filename ) :
orig_filename = filename
if filename . endswith ( ' .la ' ) or filename . endswith ( ' .lo ' ) :
with open ( filename , ' r ' ) as fd :
for line in fd :
line = line . strip ( )
if line . startswith ( ' # ' ) or line == ' ' or ' = ' not in line :
continue
var , val = line . split ( ' = ' , 1 )
if var not in [ ' library_names ' , ' pic_object ' ] :
continue
if val . startswith ( " ' " ) or val . startswith ( ' " ' ) :
val = val [ 1 : - 1 ]
if var == ' pic_object ' :
filename = os . path . join ( os . path . dirname ( filename ) , val )
break
val = val . strip ( ) . split ( ) [ 0 ]
filename = os . path . join ( os . path . dirname ( filename ) , ' .libs ' , val )
break
else :
raise ValueError ( ' could not process libtool file " %s " ' % orig_filename )
while True :
with open ( filename , ' rb ' ) as fd :
hdr = fd . read ( 4 )
if hdr == b ' \x7f ELF ' :
self . load_elf ( filename , orig_filename )
return
if hdr [ : 2 ] == b ' #! ' :
path , name = os . path . split ( filename )
filename = os . path . join ( path , ' .libs ' , name )
continue
if hdr [ : 1 ] == b ' { ' :
with open ( filename , ' r ' ) as fd :
self . load_json ( fd )
return
raise ValueError ( ' cannot determine file type for %s ' % ( filename ) )
def load_elf ( self , filename , orig_filename ) :
edf = ELFDissectFile ( filename )
edf . orig_filename = orig_filename
note = edf . _elffile . find_note ( ' FRRouting ' , ' XREF ' )
if note is not None :
endian = ' > ' if edf . _elffile . bigendian else ' < '
mem = edf . _elffile [ note ]
if edf . _elffile . elfclass == 64 :
start , end = struct . unpack ( endian + ' QQ ' , mem )
start + = note . start
end + = note . start + 8
else :
start , end = struct . unpack ( endian + ' II ' , mem )
start + = note . start
end + = note . start + 4
ptrs = edf . iter_data ( XrefPtr , slice ( start , end ) )
else :
xrefarray = edf . get_section ( ' xref_array ' )
if xrefarray is None :
raise ValueError ( ' file has neither xref note nor xref_array section ' )
ptrs = xrefarray . iter_data ( XrefPtr )
for ptr in ptrs :
if ptr . xref is None :
print ( ' NULL xref ' )
continue
self . _xrefs . append ( ptr . xref )
container = ptr . xref . container ( )
if container is None :
continue
container . to_dict ( self )
return edf
def load_json ( self , fd ) :
data = json . load ( fd )
for uid , items in data [ ' refs ' ] . items ( ) :
myitems = self [ ' refs ' ] . setdefault ( uid , [ ] )
for item in items :
if item in myitems :
continue
myitems . append ( item )
for cmd , items in data [ ' cli ' ] . items ( ) :
self [ ' cli ' ] . setdefault ( cmd , { } ) . update ( items )
return data
def check ( self , checks ) :
for xref in self . _xrefs :
yield from xref . check ( checks )
def main ( ) :
argp = argparse . ArgumentParser ( description = ' FRR xref ELF extractor ' )
argp . add_argument ( ' -o ' , dest = ' output ' , type = str , help = ' write JSON output ' )
argp . add_argument ( ' --out-by-file ' , type = str , help = ' write by-file JSON output ' )
2022-10-04 18:44:36 +02:00
argp . add_argument ( ' -c ' , dest = ' vtysh_cmds ' , type = str , help = ' write vtysh_cmd.c ' )
2020-04-30 21:33:58 +02:00
argp . add_argument ( ' -Wlog-format ' , action = ' store_const ' , const = True )
2021-02-14 00:53:27 +01:00
argp . add_argument ( ' -Wlog-args ' , action = ' store_const ' , const = True )
2021-04-13 20:57:25 +02:00
argp . add_argument ( ' -Werror ' , action = ' store_const ' , const = True )
2020-04-30 21:33:58 +02:00
argp . add_argument ( ' --profile ' , action = ' store_const ' , const = True )
argp . add_argument ( ' binaries ' , metavar = ' BINARY ' , nargs = ' + ' , type = str , help = ' files to read (ELF files or libtool objects) ' )
args = argp . parse_args ( )
if args . profile :
import cProfile
cProfile . runctx ( ' _main(args) ' , globals ( ) , { ' args ' : args } , sort = ' cumtime ' )
else :
_main ( args )
def _main ( args ) :
errors = 0
xrelfo = Xrelfo ( )
for fn in args . binaries :
try :
xrelfo . load_file ( fn )
except :
errors + = 1
sys . stderr . write ( ' while processing %s : \n ' % ( fn ) )
traceback . print_exc ( )
for option in dir ( args ) :
2021-04-13 20:57:25 +02:00
if option . startswith ( ' W ' ) and option != ' Werror ' :
2020-04-30 21:33:58 +02:00
checks = sorted ( xrelfo . check ( args ) )
sys . stderr . write ( ' ' . join ( [ c [ - 1 ] for c in checks ] ) )
2021-04-13 20:57:25 +02:00
if args . Werror and len ( checks ) > 0 :
errors + = 1
2020-04-30 21:33:58 +02:00
break
refs = xrelfo [ ' refs ' ]
counts = { }
for k , v in refs . items ( ) :
strs = set ( [ i [ ' fmtstring ' ] for i in v ] )
if len ( strs ) != 1 :
print ( ' \033 [31;1m %s \033 [m ' % k )
counts [ k ] = len ( v )
out = xrelfo
outbyfile = { }
for uid , locs in refs . items ( ) :
for loc in locs :
filearray = outbyfile . setdefault ( loc [ ' file ' ] , [ ] )
loc = dict ( loc )
del loc [ ' file ' ]
filearray . append ( loc )
for k in outbyfile . keys ( ) :
outbyfile [ k ] = sorted ( outbyfile [ k ] , key = lambda x : x [ ' line ' ] )
if errors :
sys . exit ( 1 )
if args . output :
with open ( args . output + ' .tmp ' , ' w ' ) as fd :
2022-10-04 18:01:08 +02:00
json . dump ( out , fd , indent = 2 , sort_keys = True , * * json_dump_args )
2020-04-30 21:33:58 +02:00
os . rename ( args . output + ' .tmp ' , args . output )
if args . out_by_file :
with open ( args . out_by_file + ' .tmp ' , ' w ' ) as fd :
2022-10-04 18:01:08 +02:00
json . dump ( outbyfile , fd , indent = 2 , sort_keys = True , * * json_dump_args )
2020-04-30 21:33:58 +02:00
os . rename ( args . out_by_file + ' .tmp ' , args . out_by_file )
2022-10-04 18:44:36 +02:00
if args . vtysh_cmds :
with open ( args . vtysh_cmds + ' .tmp ' , ' w ' ) as fd :
CommandEntry . run ( out , fd )
os . rename ( args . vtysh_cmds + ' .tmp ' , args . vtysh_cmds )
if args . Werror and CommandEntry . warn_counter :
sys . exit ( 1 )
2020-04-30 21:33:58 +02:00
if __name__ == ' __main__ ' :
main ( )