From 0d28a4ac085a91d8c72ec75c730b6acf0df47cf1 Mon Sep 17 00:00:00 2001 From: Steven Dillingham Date: Wed, 25 Feb 2026 17:06:01 -0500 Subject: [PATCH 1/3] bsubkill utility for managing orderly cleanup of Usage: bsubmit [--user ] ed jobs --- bsubkill/Makefile | 10 ++ bsubkill/README.md | 63 +++++++ bsubkill/bsubkill.cpp | 397 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 470 insertions(+) create mode 100644 bsubkill/Makefile create mode 100644 bsubkill/README.md create mode 100644 bsubkill/bsubkill.cpp diff --git a/bsubkill/Makefile b/bsubkill/Makefile new file mode 100644 index 0000000..441f078 --- /dev/null +++ b/bsubkill/Makefile @@ -0,0 +1,10 @@ +bsubkill: + g++ -o bsubkill bsubkill.cpp + +install: bsubkill + cp -f bsubkill $(LSF_BINDIR) + chown root:root $(LSF_BINDIR)/bsubkill + chmod u+s $(LSF_BINDIR)/bsubkill + +clean: + rm -rf bsubkill \ No newline at end of file diff --git a/bsubkill/README.md b/bsubkill/README.md new file mode 100644 index 0000000..40aa2d8 --- /dev/null +++ b/bsubkill/README.md @@ -0,0 +1,63 @@ +# bsubkill + +bsubkill is a wrapper of bkill. It allows a user to kill another user's job. + +If you would like to contribute, you must follow the DCO process in the attached [DCO Readme file](https://github.com/IBMSpectrumComputing/lsf-utils/blob/master/IBMDCO.md) in the root of this repository. It essentially requires you to provide a Sign Off line in the notes of your pull request stating that the work does not infinge on the work of others. For more details, refer to the DCO Readme file. + +## Release Information + +* IBM Spectrum LSF bsubkill +* Supporting LSF Release: 10.1 +* Version: 1.0 +* Publication date: 25 Feb 2026 +* Last modified: 25 Feb 2026 + +## Introduction + +You might want to kill a job that was submitted using bsubmit for a different user. + +To do this, this package introduces a bkill wrapper with the setuid bit set. The wrapper retrieves the user of the job id and verifies if the mapping is valid using a configuration file. + +## Building and installing + +Before building, set the LSF environment variables: + + $ source profile.lsf + +To build and install, go to the main source directory and run the following commands: + + $ make + $ make install + +A new executable file named bsubkill is installed in the $LSF_BINDIR directory. Ensure that the file is owned by root and has the setuid bit enabled. To do this, you must have root privileges. + +## Configuring and running + +The bsubkill utility uses the mapping policy as bsubmit. See [Configuring and running](https://github.com/IBMSpectrumComputing/lsf-utils/blob/master/bsubmit/README.md#configuring-and-running) for configuring the mapping policy for both utilities. + +The bsubkill only supports killing a job by its job id with the following command: + + $ bsubkill 12345678 + + +## Community contribution requirements + +Community contributions to this repository must follow the [IBM Developer's Certificate of Origin (DCO)](https://github.com/IBMSpectrumComputing/lsf-utils/blob/master/IBMDCO.md) process and only through GitHub pull requests: + +1. Contributor proposes new code to the community. + +2. Contributor signs off on contributions (that is, attaches the DCO to ensure the contributor is either the code originator or has rights to publish. The template of the DCO is included in this package). + +3. IBM Spectrum LSF Development reviews the contributions to check for the following: + i) Applicability and relevancy of functional content + ii) Any obvious issues + +4. If accepted, contribution is posted. If rejected, work goes back to the contributor and is not merged. + +## Copyright + +©Copyright IBM Corporation 2020 + +U.S. Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp. + +IBM®, the IBM logo, and ibm.com® are trademarks of International Business Machines Corp., registered in many jurisdictions worldwide. Other product and service names might be trademarks of IBM or other companies. A current list of IBM trademarks is available on the Web at "Copyright and trademark information" at . \ No newline at end of file diff --git a/bsubkill/bsubkill.cpp b/bsubkill/bsubkill.cpp new file mode 100644 index 0000000..0308037 --- /dev/null +++ b/bsubkill/bsubkill.cpp @@ -0,0 +1,397 @@ +/* + * Copyright International Business Machines Corp, 2020. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + + +const bool BSUBKILL_DEBUG = false; +const char* whitespace = " \t\n\r\f\v"; + +inline std::string trim(std::string s) { + if (!s.empty()) + s.erase(0, s.find_first_not_of(whitespace)); + if (!s.empty()) + s.erase(s.find_last_not_of(whitespace) + 1); + + return s; +} + +std::string getClusterName() +{ + std::string cname, readline; + FILE * fp = popen("lsid", "r"); + if (fp != NULL) { + char buffer[4096]; + std::string namepre = "My cluster name is "; + while(fgets(buffer, sizeof(buffer) - 1, fp) != NULL) { + readline = buffer; + size_t pos = readline.find(namepre); + if (pos != std::string::npos) { + cname = readline.substr(pos + namepre.length()); + while ((pos = cname.find('\n')) != std::string::npos) { + cname.erase(pos); + } + break; + } + } + } + + pclose(fp); + return cname; +} + +std::string getMappingFilePath() +{ + std::string filepath; + char buf[1024]; + + char *confdir = getenv("LSF_ENVDIR"); + if (confdir == NULL) { + // try uniform path + memset(buf, 0, sizeof(buf)); + int count = readlink("/etc/lsf.conf", buf, sizeof(buf) - 1); + if (count > 0) { + char * pos = strstr(buf, "/lsf.conf"); + if (pos != NULL) { + *pos = '\0'; + confdir = buf; + } + } + } + if (confdir != NULL) { + filepath = confdir; + filepath.append("/lsf.usermapping"); + } + return filepath; +} + + + +void strtrim(std::string & str) +{ + size_t pos; + while ((pos = str.find("\t")) != std::string::npos) { + str.replace(pos, 1, " "); + } + + str.erase(0, str.find_first_not_of(" ")); + str.erase(str.find_last_not_of(" ") + 1); + + while ((pos = str.find(", ")) != std::string::npos) { + str.replace(pos, 2, ","); + } + while ((pos = str.find(" ,")) != std::string::npos) { + str.replace(pos, 2, ","); + } + while ((pos = str.find(" =")) != std::string::npos) { + str.replace(pos, 2, "="); + } + while ((pos = str.find("= ")) != std::string::npos) { + str.replace(pos, 2, "="); + } + while ((pos = str.find(" ")) != std::string::npos) { + str.replace(pos, 2, " "); + } +} + + +void split(std::vector & tokens, const std::string & str, char delim = ' ') +{ + std::stringstream ss(str); + std::string token; + while (getline(ss, token, delim)) { + strtrim(token); + if (!token.empty()) { + tokens.push_back(token); + } + } +} + + +bool userMatch(std::string user, std::string ug) +{ + if (user.compare(ug) == 0) { + return true; + } + + struct passwd * pw = getpwnam(user.c_str()); + if(pw == NULL){ + return false; + } + + int ngroups = 0; + getgrouplist(pw->pw_name, pw->pw_gid, NULL, &ngroups); + gid_t groups[ngroups]; + getgrouplist(pw->pw_name, pw->pw_gid, groups, &ngroups); + + for (int i = 0; i < ngroups; i++){ + struct group * gr = getgrgid(groups[i]); + if(gr == NULL){ + continue; + } + if (ug.compare(gr->gr_name) == 0) { + return true; + } + } + + return false; +} + +std::vector getClusterAdmin(std::string clustername) +{ + std::vector adminuids; + + std::string clustercmd = "lsclusters -l " + clustername; + FILE * fp = popen(clustercmd.c_str(), "r"); + if (fp != NULL) { + char buffer[4096]; + std::string adminpre = "LSF administrators: "; + std::string readline; + while(fgets(buffer, sizeof(buffer) - 1, fp) != NULL) { + readline = buffer; + size_t pos = readline.find(adminpre); + if (pos != std::string::npos) { + std::string adminstr = readline.substr(pos + adminpre.length()); + while ((pos = adminstr.find('\n')) != std::string::npos) { + adminstr.erase(pos); + } + std::vector nametokens; + split(nametokens, adminstr, ' '); + for (int i = 0; i < nametokens.size(); i++) { + struct passwd *pw = getpwnam(nametokens[i].c_str()); + if (pw == NULL) { + continue; + } + adminuids.push_back(pw->pw_uid); + } + break; + } + } + } + + pclose(fp); + return adminuids; +} + +bool fileExists(std::string fpath) +{ + struct stat st; + return (stat(fpath.c_str(), &st) == 0); +} + + +bool fileHasCorrectAttr(std::string fpath, std::vector owners) +{ + bool validowner = false, validmode = false; + struct stat st; + + if (stat(fpath.c_str(), &st) != -1){ + for (int i = 0; i < owners.size(); i++) { + if (st.st_uid == owners[i]) { + validowner = true; + break; + } + } + if (((st.st_mode & S_IRWXU) == (S_IRUSR | S_IWUSR)) && + ((st.st_mode & S_IRWXG) == S_IRGRP) && + ((st.st_mode & S_IRWXO) == S_IROTH)) { + validmode = true; + } + } + + return (validowner && validmode); +} + +bool verifyUserMapping(std::string fpath, std::string execName) +{ + bool result = false; + + struct passwd * userpsw = getpwuid(getuid()); + if (userpsw == NULL) { + std::cout << "Failed to retrieve current user" << std::endl; + return false; + } + std::string userName = userpsw->pw_name; + + std::ifstream ifs; + ifs.open(fpath.c_str(), std::ifstream::in); + if (!ifs.is_open()) { + std::cout << "Failed to open user mapping file in " << fpath << std::endl; + return false; + } + + std::string buf; + while (getline(ifs, buf)) { + strtrim(buf); + if (buf.empty() || buf[0] == '#') { + continue; + } + + std::vector tokens; + split(tokens, buf); + if (tokens.size() != 2) { + continue; + } + + std::vector userTokens, execTokens; + split(userTokens, tokens[0], ','); + split(execTokens, tokens[1], ','); + bool matchUser = false, matchExec = false; + + for(int i = 0; i < userTokens.size(); ++i) { + if (userMatch(userName, userTokens[i])) { + matchUser = true; + break; + } + } + if (matchUser) { + for (int j = 0; j < execTokens.size(); ++j) { + if (userMatch(execName, execTokens[j])) { + matchExec = true; + break; + } + } + } + + if (matchUser && matchExec) { + result = true; + break; + } + } + + ifs.close(); + return result; +} + + +int runcmd(std::string cmd, std::ostream &os) +{ + int retVal = -1; + FILE * fp = popen(cmd.c_str(), "r"); + if (fp != NULL) { + char buffer[4096]; + while(fgets(buffer, sizeof(buffer) - 1, fp) != NULL) { + os << buffer; + } + retVal = pclose(fp); + } + return retVal; +} + +int runcmd(std::string cmd) +{ + return runcmd(cmd, std::cout); +} + +int changeUser(const char * execUser) +{ + if (geteuid() != 0) { + std::cout << "This program must be run with root privileges." << std::endl; + return -1; + } + + struct passwd * execpsw = getpwnam(execUser); + if (execpsw == NULL) { + std::cout << "The user name " << execUser << " is not valid." << std::endl; + return -1; + } + uid_t execuid = execpsw->pw_uid; + + std::string clustername = getClusterName(); + if (clustername.empty()) { + //std::cout << "Cannot get LSF cluster name" << std::endl; + return -1; + } + + std::vector admins = getClusterAdmin(clustername); + if (admins.size() < 1) { + //std::cout << "Cannot get cluster admins" << std::endl; + return -1; + } + + std::string mappingfile = getMappingFilePath(); + if (mappingfile.empty()) { + std::cout << "The LSF environment is not ready. Source the LSF environment to resolve this issue." << std::endl; + return -1; + } + + if (!fileExists(mappingfile)) { + std::cout << "The user mapping file " << mappingfile << " does not exist." << std::endl; + return -1; + } + + if (!fileHasCorrectAttr(mappingfile, admins)) { + std::cout << "User mapping file must be owned by the LSF administrators with '644' permissions." << std::endl; + return -1; + } + + if (verifyUserMapping(mappingfile, execUser)) { + setuid(execuid); + } else { + std::cout << "Failed to kill the job for user " << execUser << std::endl; + return -1; + } + if (BSUBKILL_DEBUG) + std::cout << "now uid is " << getuid() << std::endl; + + return 0; +} + +int main(int argc, char **argv) +{ + std::string usage("Usage: bsubkill job-id"); + + if (argc < 2) { + std::cout << usage << std::endl; + return -1; + } + + std::string bjobscmd = "bjobs -noheader -o \"user\" "; + bjobscmd.append(argv[1]); + + std::ostringstream oss; + int exit_code = runcmd(bjobscmd, oss); + if (exit_code == 0) { + std::string user = trim(oss.str()); + + if (user.length() == 0) { + return -1; + } + + if (changeUser(user.c_str()) != 0) { + return -1; + } + + std::string bkillcmd = "bkill "; + bkillcmd.append(argv[1]); + exit_code = runcmd(bkillcmd); + } + + return exit_code; +} \ No newline at end of file From 728972db54f7b34f9c27f5b751ba2520b1886726 Mon Sep 17 00:00:00 2001 From: Steven Dillingham Date: Thu, 26 Feb 2026 08:59:54 -0500 Subject: [PATCH 2/3] Update the root repository README --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 7e22c8f..b67367d 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,9 @@ - [bsubmit](https://github.com/IBMSpectrumComputing/lsf-utils/tree/master/bsubmit) A setuid tool to submit LSF jobs on behalf of other users. +- [bsubkill](https://github.com/IBMSpectrumComputing/lsf-utils/tree/master/bsubkill) +A setuid tool to kill other users' LSF jobs. + - [LSF chatops](https://github.com/IBMSpectrumComputing/lsf-utils/tree/master/chatops/errbot) An `Errbot` plugin to help you talking with your `LSF` cluster by `Slack` from anywhere. From f9971e29775af8a76fe1d25969bdf88c52d2fd51 Mon Sep 17 00:00:00 2001 From: Steven Dillingham Date: Thu, 26 Feb 2026 11:00:12 -0500 Subject: [PATCH 3/3] return the exit code from status --- bsubkill/bsubkill.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bsubkill/bsubkill.cpp b/bsubkill/bsubkill.cpp index 0308037..100d80e 100644 --- a/bsubkill/bsubkill.cpp +++ b/bsubkill/bsubkill.cpp @@ -292,16 +292,17 @@ bool verifyUserMapping(std::string fpath, std::string execName) int runcmd(std::string cmd, std::ostream &os) { - int retVal = -1; FILE * fp = popen(cmd.c_str(), "r"); if (fp != NULL) { char buffer[4096]; while(fgets(buffer, sizeof(buffer) - 1, fp) != NULL) { os << buffer; } - retVal = pclose(fp); + int status = pclose(fp); + return WEXITSTATUS(status); + } else { + throw std::runtime_error("popen() failed!"); } - return retVal; } int runcmd(std::string cmd)