├── .editorconfig ├── .github └── workflows │ └── regress.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── expected └── pg_remote_exec.out ├── pg_remote_exec--1.0.sql ├── pg_remote_exec.c ├── pg_remote_exec.control ├── pg_remote_exec.md └── sql └── pg_remote_exec.sql /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{c,h,l,y,pl,pm}] 4 | indent_style = tab 5 | indent_size = tab 6 | tab_width = 4 7 | 8 | [*.{sgml,xml}] 9 | indent_style = space 10 | indent_size = 1 11 | 12 | [*.xsl] 13 | indent_style = space 14 | indent_size = 2 15 | -------------------------------------------------------------------------------- /.github/workflows/regress.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | defaults: 13 | run: 14 | shell: sh 15 | 16 | strategy: 17 | matrix: 18 | pgversion: 19 | - 16 20 | - 15 21 | - 14 22 | - 13 23 | - 12 24 | 25 | env: 26 | PGVERSION: ${{ matrix.pgversion }} 27 | 28 | steps: 29 | - name: checkout 30 | uses: actions/checkout@v4 31 | 32 | - name: install pg 33 | run: | 34 | sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -v $PGVERSION -p -i 35 | sudo -u postgres createuser -s "$USER" 36 | 37 | - name: build 38 | run: | 39 | make PROFILE="-Werror" 40 | sudo -E make install 41 | 42 | - name: test 43 | run: | 44 | sudo pg_ctlcluster $PGVERSION main restart 45 | make installcheck 46 | 47 | - name: show regression diffs 48 | if: ${{ failure() }} 49 | run: | 50 | cat regression.diffs 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Clangd 2 | .cache/ 3 | .clangd 4 | compile_commands.json 5 | 6 | # Build Output 7 | *.bc 8 | *.o 9 | *.so 10 | .deps/ 11 | 12 | # Regression Tests Output 13 | results/ 14 | regression.* 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The PostgreSQL License 2 | 3 | Copyright (c) 2016-2024, CYBERTEC PostgreSQL International GmbH 4 | 5 | Permission to use, copy, modify, and distribute this software and its 6 | documentation for any purpose, without fee, and without a written agreement is 7 | hereby granted, provided that the above copyright notice and this paragraph and 8 | the following two paragraphs appear in all copies. 9 | 10 | IN NO EVENT SHALL CYBERTEC PostgreSQL International GmbH BE LIABLE TO ANY PARTY 11 | FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING 12 | LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, 13 | EVEN IF CYBERTEC PostgreSQL International GmbH HAS BEEN ADVISED OF THE 14 | POSSIBILITY OF SUCH DAMAGE. 15 | 16 | CYBERTEC PostgreSQL International GmbH SPECIFICALLY DISCLAIMS ANY WARRANTIES, 17 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 18 | FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS 19 | IS" BASIS, AND CYBERTEC PostgreSQL International GmbH HAS NO OBLIGATIONS TO 20 | PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MODULE_big = pg_remote_exec 2 | OBJS = pg_remote_exec.o 3 | 4 | EXTENSION = pg_remote_exec 5 | DATA = pg_remote_exec--1.0.sql 6 | REGRESS = pg_remote_exec 7 | DOCS = pg_remote_exec.md 8 | 9 | PG_CFLAGS = -Wformat 10 | 11 | USE_PGXS = 1 12 | ifdef USE_PGXS 13 | PG_CONFIG = pg_config 14 | PGXS := $(shell $(PG_CONFIG) --pgxs) 15 | include $(PGXS) 16 | else 17 | subdir = contrib/pg_remote_exec 18 | top_builddir = ../.. 19 | include $(top_builddir)/src/Makefile.global 20 | include $(top_srcdir)/contrib/contrib-global.mk 21 | endif 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pg_remote_exec 2 | 3 | PostgreSQL extension to allow shell command execution via SQL query. 4 | 5 | # INSTALL 6 | 7 | Install PostgreSQL before proceeding. Make sure to have `pg_config` binary, 8 | these are typically included in `-dev` and `-devel` packages. 9 | 10 | ```bash 11 | git clone https://github.com/cybertec-postgresql/pg_remote_exec.git 12 | cd pg_remote_exec 13 | make 14 | sudo make install 15 | ``` 16 | 17 | # CONFIGURE 18 | 19 | Execute as superuser: 20 | 21 | ``` 22 | CREATE EXTENSION pg_remote_exec; 23 | ``` 24 | 25 | # USAGE 26 | 27 | ``` 28 | postgres=# SELECT pg_remote_exec('date'); 29 | pg_remote_exec 30 | ---------------- 31 | 0 32 | (1 row) 33 | 34 | postgres=# SELECT pg_remote_exec_fetch('date', 't'); 35 | pg_remote_exec_fetch 36 | ─────────────────────────────── 37 | Fri Aug 5 17:41:07 EEST 2016 38 | (1 row) 39 | ``` 40 | 41 | # REFERENCE 42 | 43 | ## Functions 44 | 45 | * `pg_remote_exec(text)`: executes the command and returns the shell 46 | exit code. 47 | * `pg_remote_exec_fetch(text, boolean)`: executes the command and returns 48 | output as text. The boolean parameter dictates whether to return output in 49 | case of non-zero exit code. 50 | -------------------------------------------------------------------------------- /expected/pg_remote_exec.out: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS pg_remote_exec; 2 | select pg_remote_exec('date'); 3 | pg_remote_exec 4 | ---------------- 5 | 0 6 | (1 row) 7 | 8 | select pg_remote_exec_fetch('echo "test"', 't'); 9 | pg_remote_exec_fetch 10 | ---------------------- 11 | test 12 | (1 row) 13 | 14 | select pg_remote_exec_fetch('this_cmd_does_not_exist', 'f'); 15 | ERROR: Failed to read command output. 16 | -------------------------------------------------------------------------------- /pg_remote_exec--1.0.sql: -------------------------------------------------------------------------------- 1 | -- complain if script is sourced in psql, rather than via CREATE EXTENSION 2 | \echo Use "CREATE EXTENSION pg_remote_exec" to load this file. \quit 3 | 4 | CREATE FUNCTION pg_remote_exec(text) 5 | RETURNS int4 6 | AS 'MODULE_PATHNAME', 'pg_remote_exec' 7 | LANGUAGE C VOLATILE STRICT; 8 | 9 | CREATE FUNCTION pg_remote_exec_fetch(text, bool) 10 | RETURNS SETOF text 11 | AS 'MODULE_PATHNAME', 'pg_remote_exec_fetch' 12 | LANGUAGE C VOLATILE STRICT; 13 | -------------------------------------------------------------------------------- /pg_remote_exec.c: -------------------------------------------------------------------------------- 1 | #include "postgres.h" 2 | 3 | #include "catalog/pg_authid.h" 4 | #include "funcapi.h" 5 | #include "miscadmin.h" 6 | #include "utils/acl.h" 7 | #include "utils/builtins.h" 8 | #include "utils/elog.h" 9 | 10 | PG_MODULE_MAGIC; 11 | 12 | #if PG_VERSION_NUM < 120000 13 | #error "Unsupported PostgreSQL Version" 14 | #endif 15 | 16 | typedef struct OutputContext 17 | { 18 | FILE *fp; 19 | char *line; 20 | size_t len; 21 | } OutputContext; 22 | 23 | /* Verify the user has sufficent privileges. */ 24 | static inline void check_privileges(void); 25 | 26 | /* Execute a given shell command and return status. */ 27 | PG_FUNCTION_INFO_V1(pg_remote_exec); 28 | /* Execute a given shell command and return its stdout as a string. */ 29 | PG_FUNCTION_INFO_V1(pg_remote_exec_fetch); 30 | 31 | inline void 32 | check_privileges(void) 33 | { 34 | if (!has_privs_of_role(GetUserId(), 35 | #if PG_VERSION_NUM < 140000 36 | DEFAULT_ROLE_EXECUTE_SERVER_PROGRAM 37 | #else 38 | ROLE_PG_EXECUTE_SERVER_PROGRAM 39 | #endif 40 | )) 41 | ereport(ERROR, 42 | errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), 43 | errmsg("insufficient privileges"), 44 | errhint("Only superusers" 45 | " and members of pg_execute_server_program" 46 | " may execute this function.")); 47 | } 48 | 49 | Datum 50 | pg_remote_exec(PG_FUNCTION_ARGS) 51 | { 52 | int result; 53 | char *exec_string; 54 | 55 | check_privileges(); 56 | 57 | exec_string = text_to_cstring(PG_GETARG_TEXT_PP(0)); 58 | result = system(exec_string); 59 | 60 | pfree(exec_string); 61 | PG_RETURN_INT32(result); 62 | } 63 | 64 | Datum 65 | pg_remote_exec_fetch(PG_FUNCTION_ARGS) 66 | { 67 | FuncCallContext *funcctx; 68 | OutputContext *ocxt; 69 | ssize_t read; 70 | text *result; 71 | bool ignore_errors; 72 | 73 | check_privileges(); 74 | 75 | ignore_errors = PG_GETARG_BOOL(1); 76 | 77 | if (SRF_IS_FIRSTCALL()) 78 | { 79 | char *exec_string; 80 | MemoryContext oldcontext; 81 | 82 | /* 83 | * This chunk will eventually be freed by PG executor. I'm not sure if 84 | * it's wise to free it immediately after the popen() call - might 85 | * libc still need it during output retrieval? In any case, we don't 86 | * need to access the chunk anymore, so it's ok to define it as a 87 | * local variable here. 88 | */ 89 | exec_string = text_to_cstring(PG_GETARG_TEXT_PP(0)); 90 | 91 | funcctx = SRF_FIRSTCALL_INIT(); 92 | 93 | /* 94 | * palloc0() (unlike palloc()) sets the allocated chung to all zeroes, 95 | * so we don't need to explicitly set ocxt->line to NULL, nor 96 | * ocxt->len to 0. 97 | */ 98 | oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx); 99 | ocxt = (OutputContext *) palloc0(sizeof(OutputContext)); 100 | MemoryContextSwitchTo(oldcontext); 101 | errno = 0; 102 | ocxt->fp = popen(exec_string, "r"); 103 | 104 | if (ocxt->fp == NULL) 105 | { 106 | if (ignore_errors) 107 | SRF_RETURN_DONE(funcctx); 108 | 109 | /* 110 | * When error occurs, FMGR should free the memory allocated in the 111 | * current transaction. 112 | */ 113 | ereport(ERROR, 114 | errcode(ERRCODE_SYNTAX_ERROR_OR_ACCESS_RULE_VIOLATION), 115 | errmsg("Failed to run command")); 116 | } 117 | 118 | /* Make the output context available for the next calls. */ 119 | funcctx->user_fctx = ocxt; 120 | } 121 | 122 | /* 123 | * CHECK_FOR_INTERRUPTS() would make sense here, but I don't know how to 124 | * ensure freeing of ocxt->line and ocxt->fp, see comments below. 125 | */ 126 | 127 | funcctx = SRF_PERCALL_SETUP(); 128 | ocxt = funcctx->user_fctx; 129 | 130 | errno = 0; 131 | read = getline(&ocxt->line, &ocxt->len, ocxt->fp); 132 | /* This is serious enough to bring down the whole PG backend. */ 133 | if (errno == EINVAL) 134 | { 135 | ereport(FATAL, errcode(ERRCODE_SYNTAX_ERROR_OR_ACCESS_RULE_VIOLATION), 136 | errmsg("Failed to read command output.")); 137 | } 138 | 139 | if (read == -1) 140 | { 141 | /* 142 | * The line buffer was allocated by getline(), so it's not under 143 | * control of PG memory management. It's necessary to free it 144 | * explicitly. 145 | * 146 | * The other chunks should be freed by PG executor. 147 | */ 148 | if (ocxt->line != NULL) 149 | free(ocxt->line); 150 | 151 | /* Another resource not controlled by PG. */ 152 | if (pclose(ocxt->fp) != 0 && !ignore_errors) 153 | { 154 | ereport(ERROR, 155 | errcode(ERRCODE_SYNTAX_ERROR_OR_ACCESS_RULE_VIOLATION), 156 | errmsg("Failed to read command output.")); 157 | } 158 | 159 | SRF_RETURN_DONE(funcctx); 160 | } 161 | 162 | if (ocxt->line[read - 1] == '\n') 163 | read -= 1; 164 | result = cstring_to_text_with_len(ocxt->line, read); 165 | 166 | SRF_RETURN_NEXT(funcctx, PointerGetDatum(result)); 167 | } 168 | -------------------------------------------------------------------------------- /pg_remote_exec.control: -------------------------------------------------------------------------------- 1 | comment = 'remote shell execution for non-superusers' 2 | default_version = '1.0' 3 | module_pathname = '$libdir/pg_remote_exec' 4 | relocatable = false 5 | schema = pg_catalog 6 | -------------------------------------------------------------------------------- /pg_remote_exec.md: -------------------------------------------------------------------------------- 1 | README.md -------------------------------------------------------------------------------- /sql/pg_remote_exec.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS pg_remote_exec; 2 | 3 | select pg_remote_exec('date'); 4 | 5 | select pg_remote_exec_fetch('echo "test"', 't'); 6 | 7 | select pg_remote_exec_fetch('this_cmd_does_not_exist', 'f'); 8 | --------------------------------------------------------------------------------