├── .gitignore ├── testing └── test_kernel_module │ ├── .gitignore │ ├── test.ko.xz │ ├── Makefile │ └── test.c ├── docs.go ├── go.mod ├── modprobe_test.go ├── README.md ├── load.go ├── elf_test.go ├── LICENSE ├── modprobe.go ├── go.sum ├── dep.go └── elf.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | -------------------------------------------------------------------------------- /testing/test_kernel_module/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !Makefile 4 | !test.c 5 | !test.ko.xz 6 | -------------------------------------------------------------------------------- /testing/test_kernel_module/test.ko.xz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paultag/go-modprobe/HEAD/testing/test_kernel_module/test.ko.xz -------------------------------------------------------------------------------- /docs.go: -------------------------------------------------------------------------------- 1 | // Package modprobe allows users to load and unload Linux kernel modules by 2 | // calling the relevant Linux syscalls. 3 | package modprobe 4 | -------------------------------------------------------------------------------- /testing/test_kernel_module/Makefile: -------------------------------------------------------------------------------- 1 | obj-m := test.o 2 | 3 | .PHONY: all 4 | all: clean test.ko.xz 5 | 6 | build: 7 | make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules 8 | 9 | clean: 10 | rm -f *module-common* *modules.order* *Module.symvers* *test.ko* *test.mod* *test.o* 11 | 12 | test.ko.xz: build 13 | xz -f test.ko 14 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module pault.ag/go/modprobe 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/klauspost/compress v1.17.4 7 | github.com/pierrec/lz4 v2.6.1+incompatible 8 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 9 | golang.org/x/sys v0.16.0 10 | pault.ag/go/topsort v0.1.1 11 | ) 12 | 13 | require github.com/frankban/quicktest v1.14.6 // indirect 14 | -------------------------------------------------------------------------------- /testing/test_kernel_module/test.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | MODULE_AUTHOR("Paul R. Tagliamonte"); 5 | MODULE_DESCRIPTION("Test driver"); 6 | MODULE_LICENSE("MIT"); 7 | 8 | static int __init test_init(void) { return 0; } 9 | static void __exit test_exit(void) {} 10 | 11 | module_init(test_init); 12 | module_exit(test_exit); 13 | -------------------------------------------------------------------------------- /modprobe_test.go: -------------------------------------------------------------------------------- 1 | package modprobe 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | ) 8 | 9 | func TestInit(t *testing.T) { 10 | if os.Getenv("TEST_MODULE_INIT") == "" { 11 | t.Skipf("Skipping module init testing") 12 | } 13 | 14 | modulePath := filepath.Join("testing", "test_kernel_module", "test.ko.xz") 15 | 16 | f, err := os.Open(modulePath) 17 | if err != nil { 18 | t.Fatalf("failed to open test module file: %s", err) 19 | } 20 | 21 | err = Init(f, "") 22 | if err != nil { 23 | t.Fatalf("failed to init test module: %s", err) 24 | } 25 | 26 | t.Cleanup(func() { 27 | err := Remove("test") 28 | if err != nil { 29 | t.Errorf("failed to remove test module: %s", err) 30 | } 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-modprobe 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/pault.ag/go/modprobe.svg)](https://pkg.go.dev/pault.ag/go/modprobe) 4 | [![Go Report Card](https://goreportcard.com/badge/pault.ag/go/modprobe)](https://goreportcard.com/report/pault.ag/go/modprobe) 5 | 6 | Load an unload Linux kernel modules using the Linux module syscalls. 7 | 8 | This package is Linux specific. Loading a module uses the `finit` variant, 9 | which allows loading of modules by a file descriptor, rather than having to 10 | load an ELF into the process memory before loading. 11 | 12 | The ability to load and unload modules is dependent on either the `CAP_SYS_MODULE` 13 | capability, or running as root. Care should be taken to understand what security 14 | implications this has on processes that use this library. 15 | 16 | ## Setting the capability on a binary using this package 17 | 18 | ```sh 19 | $ sudo setcap cap_sys_module+ep /path/to/binary 20 | ``` 21 | -------------------------------------------------------------------------------- /load.go: -------------------------------------------------------------------------------- 1 | package modprobe 2 | 3 | import ( 4 | "os" 5 | 6 | "golang.org/x/sys/unix" 7 | ) 8 | 9 | // Load will, given a short module name (such as `g_ether`), determine where 10 | // the kernel module is located, determine any dependencies, and load all 11 | // required modules. 12 | func Load(module, params string) error { 13 | path, err := ResolveName(module) 14 | if err != nil { 15 | return err 16 | } 17 | 18 | order, err := Dependencies(path) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | paramList := make([]string, len(order)) 24 | paramList[len(order)-1] = params 25 | 26 | for i, module := range order { 27 | fd, err := os.Open(module) 28 | if err != nil { 29 | return err 30 | } 31 | /* not doing a defer since we're in a loop */ 32 | param := paramList[i] 33 | if err := Init(fd, param); err != nil && err != unix.EEXIST { 34 | fd.Close() 35 | return err 36 | } 37 | fd.Close() 38 | } 39 | 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /elf_test.go: -------------------------------------------------------------------------------- 1 | package modprobe 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestResolve(t *testing.T) { 11 | path, err := ResolveName("snd") 12 | if err != nil { 13 | t.Errorf("%s", err) 14 | } 15 | 16 | if !strings.Contains(path, "snd") { 17 | t.Fail() 18 | } 19 | 20 | _, err = os.Stat(path) 21 | if err != nil { 22 | t.Fatalf("%s", err) 23 | } 24 | } 25 | 26 | func TestResolveCompressed(t *testing.T) { 27 | moduleRoot = filepath.Join("testing", "test_kernel_module") 28 | t.Cleanup(func() { 29 | moduleRoot = getModuleRoot() 30 | }) 31 | 32 | path, err := ResolveName("test") 33 | if err != nil { 34 | t.Fatalf("%s", err) 35 | } 36 | 37 | if !strings.Contains(path, "test") { 38 | t.Fatalf("expected response path to contain 'test', got %s", path) 39 | } 40 | 41 | _, err = os.Stat(path) 42 | if err != nil { 43 | t.Fatalf("%s", err) 44 | } 45 | } 46 | 47 | func TestNotFound(t *testing.T) { 48 | _, err := ResolveName("not-found") 49 | if err == nil { 50 | t.Fail() 51 | } 52 | 53 | if !strings.Contains(err.Error(), "not-found") { 54 | t.Fail() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018, Paul R. Tagliamonte 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /modprobe.go: -------------------------------------------------------------------------------- 1 | package modprobe 2 | 3 | import ( 4 | "os" 5 | 6 | "golang.org/x/sys/unix" 7 | ) 8 | 9 | // Init will use the provide .ko file's os.File (created with os.Open or 10 | // similar), to load that kernel module into the running kernel. This may error 11 | // out for a number of reasons, such as no permission (either setcap 12 | // CAP_SYS_MODULE or run as root), the .ko being for the wrong kernel, or the 13 | // file not being a module at all. 14 | // 15 | // Any arguments to the module may be passed through `params`, such as 16 | // `file=/root/data/backing_file`. 17 | func Init(file *os.File, params string) error { 18 | content, err := readModuleFile(file) 19 | if err != nil { 20 | return err 21 | } 22 | return unix.InitModule(content, params) 23 | } 24 | 25 | // InitWithFlags will preform an Init, but allow the passing of flags to the 26 | // syscall. The `flags` parameter is a bit mask value created by ORing together 27 | // zero or more of the following flags: 28 | // 29 | // MODULE_INIT_IGNORE_MODVERSIONS - Ignore symbol version hashes 30 | // MODULE_INIT_IGNORE_VERMAGIC - Ignore kernel version magic. 31 | // 32 | // Both flags are defined in the golang.org/x/sys/unix package. 33 | func InitWithFlags(file *os.File, params string, flags int) error { 34 | return unix.FinitModule(int(file.Fd()), params, flags) 35 | } 36 | 37 | // Remove will unload a loaded kernel module. If no such module is loaded, or if 38 | // the module can not be unloaded, this function will return an error. 39 | func Remove(name string) error { 40 | return unix.DeleteModule(name, 0) 41 | } 42 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 3 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 4 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 5 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 6 | github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= 7 | github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= 8 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 9 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 10 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 11 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 12 | github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= 13 | github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 14 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 15 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 16 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 17 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= 18 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= 19 | golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= 20 | golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 21 | pault.ag/go/topsort v0.1.1 h1:L0QnhUly6LmTv0e3DEzbN2q6/FGgAcQvaEw65S53Bg4= 22 | pault.ag/go/topsort v0.1.1/go.mod h1:r1kc/L0/FZ3HhjezBIPaNVhkqv8L0UJ9bxRuHRVZ0q4= 23 | -------------------------------------------------------------------------------- /dep.go: -------------------------------------------------------------------------------- 1 | package modprobe 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | "strings" 7 | 8 | "pault.ag/go/topsort" 9 | ) 10 | 11 | // Dependencies takes a path to a .ko file, determine what modules will have to 12 | // be present before loading that module, and return those modules as a slice 13 | // of strings. 14 | func Dependencies(path string) ([]string, error) { 15 | deps, err := loadDependencies() 16 | if err != nil { 17 | return nil, err 18 | } 19 | return deps.Load(path) 20 | } 21 | 22 | // simple container type that stores a mapping from an element to elements 23 | // that it depends on. 24 | type dependencies map[string][]string 25 | 26 | // top level loading of the dependency tree. this will start a network 27 | // walk the dep tree, load them into the network, and return a topological 28 | // sort of the modules. 29 | func (d dependencies) Load(name string) ([]string, error) { 30 | network := topsort.NewNetwork() 31 | if err := d.load(name, network); err != nil { 32 | return nil, err 33 | } 34 | 35 | order, err := network.Sort() 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | ret := []string{} 41 | for _, node := range order { 42 | ret = append(ret, node.Name) 43 | } 44 | return ret, nil 45 | } 46 | 47 | // add a specific dependency to the network, and recurse on the leafs. 48 | func (d dependencies) load(name string, network *topsort.Network) error { 49 | if network.Get(name) != nil { 50 | return nil 51 | } 52 | network.AddNode(name, nil) 53 | 54 | for _, dep := range d[name] { 55 | if err := d.load(dep, network); err != nil { 56 | return err 57 | } 58 | if err := network.AddEdge(dep, name); err != nil { 59 | return err 60 | } 61 | } 62 | 63 | return nil 64 | } 65 | 66 | // get a dependency map from the running kernel's modules.dep file 67 | func loadDependencies() (dependencies, error) { 68 | path := modulePath("modules.dep") 69 | 70 | file, err := os.Open(path) 71 | if err != nil { 72 | return nil, err 73 | } 74 | defer file.Close() 75 | 76 | deps := map[string][]string{} 77 | 78 | scanner := bufio.NewScanner(file) 79 | for scanner.Scan() { 80 | chunks := strings.SplitN(scanner.Text(), ":", 2) 81 | depString := strings.TrimSpace(chunks[1]) 82 | if len(depString) == 0 { 83 | continue 84 | } 85 | 86 | ret := []string{} 87 | for _, dep := range strings.Split(depString, " ") { 88 | ret = append(ret, modulePath(dep)) 89 | } 90 | deps[modulePath(chunks[0])] = ret 91 | } 92 | 93 | if err := scanner.Err(); err != nil { 94 | return nil, err 95 | } 96 | 97 | return deps, nil 98 | } 99 | -------------------------------------------------------------------------------- /elf.go: -------------------------------------------------------------------------------- 1 | package modprobe 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "debug/elf" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "io/fs" 11 | "os" 12 | "path/filepath" 13 | "regexp" 14 | "strings" 15 | 16 | "github.com/klauspost/compress/zstd" 17 | "github.com/pierrec/lz4" 18 | "github.com/xi2/xz" 19 | "golang.org/x/sys/unix" 20 | ) 21 | 22 | var ( 23 | // get the root directory for the kernel modules. If this line panics, 24 | // it's because getModuleRoot has failed to get the uname of the running 25 | // kernel (likely a non-POSIX system, but maybe a broken kernel?) 26 | moduleRoot = getModuleRoot() 27 | 28 | // koFileExtRegexp is used to match kernel extension file names. 29 | koFileExt = regexp.MustCompile(`\.ko`) 30 | ) 31 | 32 | // Get the module root (/lib/modules/$(uname -r)/) 33 | func getModuleRoot() string { 34 | uname := unix.Utsname{} 35 | if err := unix.Uname(&uname); err != nil { 36 | panic(err) 37 | } 38 | 39 | i := 0 40 | for ; uname.Release[i] != 0; i++ { 41 | } 42 | 43 | return filepath.Join( 44 | "/lib/modules", 45 | string(uname.Release[:i]), 46 | ) 47 | } 48 | 49 | // Get a path relitive to the module root directory. 50 | func modulePath(path string) string { 51 | return filepath.Join(moduleRoot, path) 52 | } 53 | 54 | // ResolveName will, given a module name (such as `g_ether`) return an absolute 55 | // path to the .ko that provides that module. 56 | func ResolveName(name string) (string, error) { 57 | // Optimistically check via filename first. 58 | var res string 59 | err := filepath.WalkDir( 60 | moduleRoot, 61 | func(path string, info fs.DirEntry, err error) error { 62 | if strings.HasPrefix(filepath.Base(path), name+".ko") { 63 | res = path 64 | return filepath.SkipAll 65 | } 66 | return nil 67 | }) 68 | if err == nil && res != "" { 69 | fd, err := os.Open(res) 70 | if err != nil { 71 | return "", fmt.Errorf("failed to open %s: %w", res, err) 72 | } 73 | defer fd.Close() 74 | 75 | elfName, err := Name(fd) 76 | if err != nil { 77 | return "", err 78 | } 79 | if elfName == name { 80 | return res, nil 81 | } 82 | } 83 | 84 | // Fallback to full file search if no match is found. 85 | paths, err := generateMap() 86 | if err != nil { 87 | return "", err 88 | } 89 | 90 | fsPath := paths[name] 91 | if !strings.HasPrefix(fsPath, moduleRoot) { 92 | return "", fmt.Errorf("Module '%s' isn't in the module directory", name) 93 | } 94 | 95 | return fsPath, nil 96 | } 97 | 98 | // Open every single kernel module under the kernel module directory 99 | // (/lib/modules/$(uname -r)/), and parse the ELF headers to extract the 100 | // module name. 101 | func generateMap() (map[string]string, error) { 102 | return elfMap(moduleRoot) 103 | } 104 | 105 | // Open every single kernel module under the root, and parse the ELF headers to 106 | // extract the module name. 107 | func elfMap(root string) (map[string]string, error) { 108 | ret := map[string]string{} 109 | 110 | err := filepath.WalkDir( 111 | root, 112 | func(path string, info fs.DirEntry, err error) error { 113 | if !koFileExt.MatchString(path) { 114 | return nil 115 | } 116 | 117 | fd, err := os.Open(path) 118 | if err != nil { 119 | return err 120 | } 121 | defer fd.Close() 122 | 123 | name, err := Name(fd) 124 | if err != nil { 125 | /* For now, let's just ignore that and avoid adding to it */ 126 | return nil 127 | } 128 | 129 | ret[name] = path 130 | return nil 131 | }) 132 | 133 | if err != nil { 134 | return nil, err 135 | } 136 | 137 | return ret, nil 138 | } 139 | 140 | func ModInfo(file *os.File) (map[string]string, error) { 141 | content, err := readModuleFile(file) 142 | if err != nil { 143 | return nil, err 144 | } 145 | 146 | f, err := elf.NewFile(bytes.NewReader(content)) 147 | if err != nil { 148 | return nil, err 149 | } 150 | 151 | attrs := map[string]string{} 152 | 153 | sec := f.Section(".modinfo") 154 | if sec == nil { 155 | return nil, errors.New("missing modinfo section") 156 | } 157 | 158 | data, err := sec.Data() 159 | if err != nil { 160 | return nil, fmt.Errorf("failed to get section data: %w", err) 161 | } 162 | 163 | for _, info := range bytes.Split(data, []byte{0}) { 164 | if parts := strings.SplitN(string(info), "=", 2); len(parts) == 2 { 165 | attrs[parts[0]] = parts[1] 166 | } 167 | } 168 | 169 | return attrs, nil 170 | } 171 | 172 | // Name will, given a file descriptor to a Kernel Module (.ko file), parse the 173 | // binary to get the module name. For instance, given a handle to the file at 174 | // `kernel/drivers/usb/gadget/legacy/g_ether.ko`, return `g_ether`. 175 | func Name(file *os.File) (string, error) { 176 | mi, err := ModInfo(file) 177 | if err != nil { 178 | return "", fmt.Errorf("failed to get module information: %w", err) 179 | } 180 | 181 | if name, ok := mi["name"]; !ok { 182 | return "", errors.New("module information is missing name") 183 | } else { 184 | return name, nil 185 | } 186 | } 187 | 188 | // readModuleFile returns the contents of the given file descriptor, extracting 189 | // it if necessary. 190 | func readModuleFile(file *os.File) ([]byte, error) { 191 | ext := filepath.Ext(file.Name()) 192 | var r io.Reader 193 | var err error 194 | 195 | switch ext { 196 | case ".ko": 197 | r = file 198 | case ".zst": 199 | r, err = zstd.NewReader(file) 200 | case ".xz": 201 | r, err = xz.NewReader(file, 0) 202 | case ".lz4": 203 | r = lz4.NewReader(file) 204 | case ".gz": 205 | r, err = gzip.NewReader(file) 206 | default: 207 | err = fmt.Errorf("unknown module format: %s", ext) 208 | } 209 | if err != nil { 210 | return nil, fmt.Errorf("failed to extract module %s: %w", file.Name(), err) 211 | } 212 | 213 | b, err := io.ReadAll(r) 214 | if err != nil { 215 | return nil, fmt.Errorf("failed to read module %s: %w", file.Name(), err) 216 | } 217 | return b, nil 218 | } 219 | --------------------------------------------------------------------------------