0

I read this but I couldn't solve my problem. I don't know how do I use setuid

I have a small python app that runs python commands and bash command in linux machine. I want only this part below to run as normal user. App run with sudo python3 app.py because do some commands that need sudo privilege. I upload only the part I want to run as normal user.

How can I do that?

import alsaaudio

m = alsaaudio.Mixer('Capture')
m.setvolume(10) # set volume
vol = m.getvolume() # get volume float value
jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • if you run via `sudo` then you already have superuser privileges. what is the problem then? – Marcin Orlowski Dec 08 '22 at 20:03
  • Sounds like OP want's to drop privileged for part of the execution? – Sören Dec 08 '22 at 20:06
  • @Sören I want this three lines to run as normal user. As I said python has many lines that require root privilege. Those three lines I want to run as normal user without sudo. –  Dec 08 '22 at 20:10
  • @MarcinOrlowski I said those three lines I want to run as normal user. –  Dec 08 '22 at 20:11

1 Answers1

2

One can't simply change the owner of a process back and forth at will (ok, one can with the proper call: os.seteuid, I learned after starting this answer. Either way, there are more things required for this particular question than just setting the UID - the bottom part of the answer has a subprocess approach): just the root user can do that, and once its done, it is no longer root to switch back. So the trick is to call it in a process that is exclusive to run the limited tasks.

You can fork the process and call os.setuid on the child, and then terminate it:

import alsaaudio
import os
import sys
...

if not os.fork():
    # this block just runs on the child process: fork returns a non-0 PID on the parent
    os.setuid(<usernumber>)
    m = alsaaudio.Mixer('Capture')
    m.setvolume(10) # set volume
    vol = m.getvolume() # get volume float value
    sys.exit(0)  # terminates the child process. 

# normal code that will just run on the parent process (as root)
# continues:
...

Of course, this won't make the vol variable avaliable on the parent process - you have to setup a way to pass the value along.

In that case, one can use Python multiprocessing, instead of fork, with multiprocessing.Queue to send vol value, and add the needed pauses to destroy the other user process: that is good if you are writing "production quality" code that will need to handle a lot of corner cases in third party computers. If your object is a simple script for setting up stuff in your box, writting the value to a file, and reading it on parent will be easier:

import alsaaudio
import os
import sys
import tempfile
import time
import pickle


...

_, file_ = tempfile.mkstemp()

if not os.fork():
    # this block just runs on the child process: fork returns a non-0 PID on the parent
    os.setuid(<usernumber>)
    m = alsaaudio.Mixer('Capture')
    m.setvolume(10) # set volume
    vol = m.getvolume() # get volume float value
    with open(file_, "wb") as file:
         pickle.dump(vol, file)
    sys.exit(0)  # terminates the child process. 

# normal code that will just run on the parent process (as root)
# continues:

time.sleep(0.1)  # may need calibration
vol = pickle.load(open(file_, "rb"))
os.unlink(file_)
...

Given the OP comments, this is not just a matter of changing the effective UID, but also environment variables, and reimporting the alsaaudio module - a "fork" won't cut it (as env vars are not changed, and changing os.environment entries on the Pythonside will probably not be reflected on the native-code side of alsalib as it is initialized.

In this case, running the process with subprocess.Popen and ensuring the correct enviroment prior to it can attain the desired effects. Interprocess comunication can be resolved by capturing the subprocess stdout - so we can refrain from both the pickle.Also, the default subprocess call is synchronous, and no need for an arbitrary pause for the target child to initialize alsa and pick its value: the main process will continue just after the child is done, in the default call.

I am not trying alsaaudio here, it may be that more environment variables than the 2 I set are needed on the subprocess. If that is the case, just go on adding then to the env={ ...} part of the code as needed.

This sample just do what you asked, and pass the value for "vol" back - if you ever need more data, drop the "encoding" argument to the subprocess, and pickle arbitrary data to sys.stdout on the child process - you can then retriev it with pickle.loads on proc.stdout

import alsaaudio
import os
import sys

import tempfile
import time
import pickle

vol = None

def child_code():
    import alsaaudio 
    # no need to reload: when running as a subprocess, the top-level import (if any)
    # will also run as the new user
    m = alsaaudio.Mixer('Capture')
    m.setvolume(10) # set volume
    vol = m.getvolume() # get volume float value
    print(vol)
    
    ...


def parent_code():

    import subprocess
    import pwd
    target_user = 1000
    user_data = os.getpwuid(target_user).pw_dir()
    # if alsalib fails, you may need to setup other variables
    env = {"HOME": user_data, "PWD": user_data, }
    # __file__ bellow may have to be changed if this code
    # is in a submodule in a larger project.
    proc = subprocess.run(
        ["/usr/bin/env", "python3", __file__  ], 
        env=env,
        user=target_user,
        capture_output=True,
        encoding="utf-8"
    )
    vol = literal_eval(proc.stdout)
    return vol
    ...
    
if os.getuid() == 0:
    # runs  on parent, UID==1, process
    vol = parent_code()
else:
    # runs on subprocess.
    child_code()
    sys.exit(0)

...

jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • I replace `os.setuid()` with 1000 but It doesn't work. It tha same. It run as root user. –  Dec 08 '22 at 20:22
  • These lines are run as user 1000 - but it maybe that alsaaudio performs some initiaization when imported - under the root user. Try adding `from importlib import reload; reload(alsaudio)` inside the guarded block, after calling `os.setuid`. – jsbueno Dec 08 '22 at 20:26
  • It may still not work - but it is the same idea: `reload(alsaaudio)` will just reload the parent module in the project - if the initialization is performed in an internal submodule of the alsaudio, it is that specific submodule that must be reloaded. – jsbueno Dec 08 '22 at 20:27
  • It says `Failed to create secure directory (/root/.config/pulse): Permission denied` –  Dec 08 '22 at 20:34
  • so - now you know what is going on: it indeed initialize stuff on loading - but just changing the UID of the process does not help: the enrionment variables have to be changed as wll, so that it goes to teh config dir of the target user. At this point, probably using the full `subprocess.Popen` call, setting the apropriate enviroment will be better than "fork". – jsbueno Dec 08 '22 at 21:09
  • There, I added a 3rd snippet that might work as is. – jsbueno Dec 08 '22 at 21:37
  • I am sorry It doesn't work error message `alsaaudio.ALSAAudioError: Unable to find mixer control Capture,0 [default]` –  Dec 09 '22 at 14:04
  • This has nothing to do with the contents of this question. You might try a mini-program that will just do that and run as a regular user. If that works, check the ENV variables, and proceed to setting then in the proper place - either one by one untill it work. Or just try copying all environ to the subprocess and just override the home dir and pwd - change the line that creates env to: `env = os.environ.copy(); env.update({"HOME": user_data, "PWD": user_data, })` – jsbueno Dec 09 '22 at 15:56
  • @jsbuueno the problem is that line ` m = alsaaudio.Mixer('Capture')` This line is main reason that I asked here. Please help me I don't know how do I solve this problem. –  Dec 09 '22 at 16:41
  • Create a mimimum standalone program running as non-root that does just that. If that works, what you need to do is set the correct variables in the code I wrote here. Id it does not work, ask a second question to get that working. I have no more to say on what is put here, and I am starting to repeat stuff. – jsbueno Dec 09 '22 at 16:45
  • The programm run without sudo. When I run witho sudo it fails. I change your code with only one line ls -al and then it doesn't work. I don't know what line I have to change. –  Dec 09 '22 at 16:52
  • I think `parent_code` it doesn't run at all. Please would you like to help me? I try a lot of hours to solve this problem with your code but I can't –  Dec 09 '22 at 17:21
  • Sorry- UID for root is "0" not "1" - just update the "if" the select callign parent_code to 0. ( i will fix it here) – jsbueno Dec 09 '22 at 20:57
  • I changed to 0. Now the new error is `user_data = os.getpwuid(target_user).pw_dir() AttributeError: module 'os' has no attribute 'getpwuid'` –  Dec 10 '22 at 07:12