他のプロセスの環境変数を書き換える

とある事情で他プロセスの環境変数を書き換えたいことがあった。[^1],[^2]



## 基本形(C) / Ruby

https://unix.stackexchange.com/questions/38205/change-environment-of-a-running-process (や https://nanonanonano.net/linux/chenv) の情報によると、基本形は次のようであるらしい。

```sh
function chenv () {
  if [ $# -lt 3 ]; then
    echo chenv PID Name Value
  else
    sudo gdb -batch -ex "attach $1" -ex "call setenv(\"$2\",\"$3\",1)" -ex "detach"
  fi
}
```

※以下、表示されたPIDに対し`chenv PID PATH foobar`を実行して確認。

これで一部のシェルは書き換えられるらしい。
(実行はdash/bash/mksh/zsh/tcshで行えるが、書き換えが反映されるのはbash(4.4)/tcsh(6.20)のみ)

```bash:print.bash
echo $$
sleep 10
echo $PATH
```

また、Rubyも書き換えることが可能である。

tested: Ruby 2.3.3

```rb:print.rb
puts $$
1000.times{sleep 0.01}
puts ENV['PATH']
```

(次章と関連するが、以下でも書き換えられる)

```sh
function chenvrb () {
  if [ $# -lt 3 ]; then
    echo chenvrb PID Name Value
  else
    # Ruby ENV always uses getenv directly, so chenv should be enough actually.
    sudo gdb -batch -ex "attach $1" -ex "call rb_eval_string(\"ENV['$2']='$3'\")" -ex "detach"
  fi
}
```

## スクリプト言語

### Python

残念ながらRubyは書き換えられてもPythonは書き換えることができない。だが、実行方法さえわかってしまえばこちらのものである。

http://hondu.co/blog/python-and-gdb によると、PyGILState_EnsureとPyRun_SimpleStringを使うと良いらしい。

tested: Python 2.7.13 / 3.8.6

```py:print.py
import os
import time
print(os.getpid())
for _ in range(1000): time.sleep(0.01)
print(os.environ['PATH'])
```

```sh
function chenvpy () {
  if [ $# -lt 3 ]; then
    echo chenvpy PID Name Value
  else
    # Python os.environ only putenv; override must be done via Python code.
    sudo gdb -batch -ex "attach $1" -ex "call PyGILState_Ensure()" -ex "call PyRun_SimpleString(\"import os;os.environ['$2']='$3'\")" -ex "call PyGILState_Release(\$1)" -ex "detach"
  fi
}
```

### Perl

CからPerlを呼ぶ方法を検索すると、eval_pvが引っかかるが、これはPerl_eval_pvを呼ぶためのマクロである。perl/CORE/embed.hによると第一引数にaTHX_を渡す必要があり、XSUB.hおよびthread.hによるとこれは`Perl_get_context()`と等価である。

tested: Perl 5.24.1

```pl:print.pl
use 5.010;
use Time::HiRes qw(usleep);
say $$;
for(my $i=0;$i<1000;$i++){usleep(10*1000);}
say $ENV{'PATH'};
```

```sh
function chenvpl () {
  if [ $# -lt 3 ]; then
    echo chenvpl PID Name Value
  else
    sudo gdb -batch -ex "attach $1" -ex "call Perl_eval_pv(((void*(*)())Perl_get_context)(),\"\$ENV{'$2'}='$3';\",1)" -ex "detach"
  fi
}
```

### PHP

https://github.com/facebookarchive/phpembed/blob/master/src/php_cxx.cpp を検索すると、zend_eval_stringが見つかる。

- `$_ENV`変数は`-d variables_order=EGPCS`しないと使えない
- [Zend Thread Securityの関係](https://qiita.com/hnw/items/d615e0d4122f247d4c75)でPHP7+用

tested: PHP 7.0.33

```php:print.php
<?php
echo getmypid().PHP_EOL;
for($i=0;$i<1000;$i++){usleep(10*1000);}
#echo $_ENV["PATH"].PHP_EOL;
echo getenv("PATH").PHP_EOL;
```

putenvと$\_ENVの内部は別れているようなので、両方に書き込む。

```sh
function chenvphp () {
  if [ $# -lt 3 ]; then
    echo chenvphp PID Name Value
  else
    sudo gdb -batch -ex "attach $1" -ex "call zend_eval_string(\"\$_ENV['$2']='$3';putenv('$2=$3');\",0,\"\")" -ex "detach"
  fi
}
```

### Julia

chenvでも書き換えられるし以下でも書き換えられる。

tested: Julia 0.4

```jl:print.jl
println(getpid())
for i in 1:1000
  sleep(0.01)
end
println(ENV["PATH"])
```

```sh
function chenvjl () {
  if [ $# -lt 3 ]; then
    echo chenvjl PID Name Value
  else
    sudo gdb -batch -ex "attach $1" -ex "call jl_eval_string(\"ENV[\\\"$2\\\"]=\\\"$3\\\"\")" -ex "detach"
  fi
}
```

### R

chenvで書き換えられます。chenvRは複雑そうだったので断念しました(C言語拡張も書きにくい印象)。

tested: R 3.3.3

```R:print.R
print(Sys.getpid())
for(i in 1:1000){
  Sys.sleep(0.01)
}
print(Sys.getenv('PATH'))
```

### Node.js

chenvで書き換えられます。chenvjsは不可能な模様(napi_run_scriptに実行コンテキストを渡す必要が有りますが、(C言語拡張の)NAPI_MODULE_INITの引数としてしか取れないようです)。

tested: Node.js 9.11.2

```js:print.js
f=function(){console.log(process.env['PATH'])}
console.log(process.pid)
setTimeout(f,10000)
```

### Tcl

chenvで書き換えられます。chenvtclは不可能な模様(Tcl_GlobalEvalに実行コンテキストを渡す必要が有りますが、(C言語拡張の)XXX_Initの引数としてしか取れないようです)。

tested: Tcl 8.6

```tcl:print.tcl
puts [pid]
for {set i 0} {$i < 1000} {incr i} {
	after 10
}
puts $::env(PATH)
```

### Pike

getenvで第２引数を1にした場合はchenvで書き換え可能。0の場合はpike_get_interpreter_pointer()に対し http://wiki.gotpike.org/PikeDevel%2FC%20Modules%2FCalling%20Pike%20Code にあるような操作をすることで可能っぽいですが、断念しました[^3](さらに言うとpike_get_interpreter_pointer自体Pike 8.0で登場した機能)。

(half) tested: Pike 7.8

```
int main(int argc, array(string) argv){
  Stdio.stdout.write(sprintf("%d\n",getpid()));
  for(int i=0;i<1000;i++)sleep(0.01);
  Stdio.stdout.write(getenv("PATH",1));
  Stdio.stdout.write("\n");
}
```

## コンパイル言語

基本的にはこのやり方で書き換えられるのはスクリプト言語のみであるが、一部のコンパイル言語[^4]の挙動も気にはなる。といっても基本形が通用するか否かしかないが。

### C# ###

.Net Coreの実行方法は https://stackoverflow.com/a/56133028

```cs:print.cs
using System;
using System.IO;
using System.Diagnostics;
using System.Threading;

class A{
  static void Main(){
    Console.WriteLine(Process.GetCurrentProcess().Id);
    for(int i=0;i<1000;i++)Thread.Sleep(10);
    Console.WriteLine(Environment.GetEnvironmentVariable("PATH"));
  }
}
```

|Runtime|mcs 4.6.2|csc 3.7|
|:--|:--|:--|
|Mono 4.6.2|O|実行不可|
|.Net Core 3.1.403|X|X|

ランタイム実装が相当異なると推測されます。

### Java

```java:print.java
import java.lang.management.ManagementFactory;

class A{
  static final String PID = java.lang.management.ManagementFactory.getRuntimeMXBean().getName().split("@")[0];
  public static void main(String[]a){
    System.out.println(PID);
    for(int i=0;i<1000;i++)try{Thread.sleep(10);}catch(InterruptedException e){}
    System.out.println(System.getenv("PATH"));
  }
}
```

書き換え可能(Java 1.8.0_272)。

ところでaltjavaにKotlinがあり、あちらはkotlin-nativeもあるので検証に加えてみたかったですが、環境変数取得はJava頼みらしい…。

### Go

```go:print.go
package main
import "fmt"
import "os"
import "time"
func main() {
  fmt.Println(os.Getpid())
  for i:=0;i<1000;i++ {time.Sleep(time.Millisecond * 10)}
  fmt.Println(os.Getenv("PATH"))
}
```

書き換えられませんでした。

- go(1.7.4)はstatic binaryなのでそもそもコードを注入できない
- gccgo(6.3.0)はコード注入自体はできるがos.Getenv()はgetenvを呼ばない

### D

```d:print.d
import std.stdio;
import std.process;
import core.time;
import core.thread.osthread;
void main(){
  writefln("%d",getpid());
  for(int i=0;i<1000;i++)Thread.sleep( dur!("msecs")(10) );
  auto var = environment.get("PATH");
  if(!(var is null)){
    writeln(var);
  }
}
```

書き換え可能。

- dmd 2.094
- gdc 9.1.0 (`core.thread.osthread` を `core.thread` に変更)

### Rust

```rs:print.rs
use std::env;
use std::process;
use std::thread;
use std::time;

fn main() {
  println!("{}",process::id());
  for _i in 0..1000 {thread::sleep(time::Duration::from_millis(10));}
  match env::var("PATH") {
    Ok(val) => println!("{}",val),
    Err(_) => (),
  }
}
```

書き換え可能(Rust 1.33)。

### Swift

```swift:print.swift
import Foundation
print(ProcessInfo.processInfo.processIdentifier)
for _ in 0..<1000 {Thread.sleep(forTimeInterval: 0.01)}
if let value = ProcessInfo.processInfo.environment["PATH"] {
  print(value)
}
```

書き換え可能(Swift 5.3)。

### Nim

```nim:print.nim
import os
import posix
writeLine(stdout,getpid())
for i in 1..1000:sleep(10)
writeLine(stdout,getEnv("PATH"))
```

書き換え可能(Nim 0.16)。

### V

```v:print.v
import os
import time
fn main(){
  println(os.getpid())
  for i:=0;i<1000;i++ {time.sleep_ms(10)}
  println(os.getenv("PATH"))
}
```

書き換え可能(V 0.1.29 f19ca6b)。

### Crystal

```cr:print.cr
puts Process.pid
1000.times{sleep 0.01}
puts ENV["PATH"]
```

書き換え可能(Crystal 0.35.1)。

### Zig

`zig build-exe --library c`

```zig:print.zig
const std = @import("std");

extern fn getpid() i32;
pub fn main()!void{
  const stdout = std.io.getStdOut().outStream();
  try stdout.print("{}\n", .{getpid()});
  var i:i32=0;
  while(i<1000){std.os.nanosleep(0,10*1000*1000);i+=1;}
  try stdout.print("{}\n", .{std.os.getenv("PATH")});
}
```

書き換え可能(Zig 0.6.0)。

## 終わりに

最近の言語は毎回getenv()を呼ぶようにしてくれているのでしょうか!?

[^1]: オープンソースですが弊社で開発しているものではない
[^2]: 当該プロセスが既に環境変数を別変数にコピー等している場合は、上書きによる動作変更はできないことは当然承知しています。基本サブプロセス用なのかなと思います
[^3]: そもそもですが、某sup...のようなシステムアプリをPikeで作ることはまずないから良いと思う。。
[^4]: C++は標準だとCライブラリを使うしかない(≒基本形が通用することが自明)ので調査対象でない