Description:
add option -A <opt> to box. This options allow more argument to be explicitly passed to the program
We have to use this because if the argument we wish to pass to the program is option (in -? format),
box will intepret it as its option and failed accordingly.
be noted that, by the definition of getopt, these options will be put after original argument
(check the code for more info)
Commit status:
[Not Reviewed]
References:
Diff options:
Comments:
0 Commit comments
0 Inline Comments
Unresolved TODOs:
There are no unresolved TODOs
r186:c8d646326d0a - - 1 file changed: 17 inserted, 4 deleted
@@ -891,857 +891,870 | |||
|
891 | 891 | } |
|
892 | 892 | else |
|
893 | 893 | r->val = NULL; |
|
894 | 894 | *last_env_rule = r; |
|
895 | 895 | last_env_rule = &r->next; |
|
896 | 896 | r->next = NULL; |
|
897 | 897 | return 1; |
|
898 | 898 | } |
|
899 | 899 | |
|
900 | 900 | static int |
|
901 | 901 | match_env_var(char *env_entry, struct env_rule *r) |
|
902 | 902 | { |
|
903 | 903 | if (strncmp(env_entry, r->var, r->var_len)) |
|
904 | 904 | return 0; |
|
905 | 905 | return (env_entry[r->var_len] == '='); |
|
906 | 906 | } |
|
907 | 907 | |
|
908 | 908 | static void |
|
909 | 909 | apply_env_rule(char **env, int *env_sizep, struct env_rule *r) |
|
910 | 910 | { |
|
911 | 911 | // First remove the variable if already set |
|
912 | 912 | int pos = 0; |
|
913 | 913 | while (pos < *env_sizep && !match_env_var(env[pos], r)) |
|
914 | 914 | pos++; |
|
915 | 915 | if (pos < *env_sizep) |
|
916 | 916 | { |
|
917 | 917 | (*env_sizep)--; |
|
918 | 918 | env[pos] = env[*env_sizep]; |
|
919 | 919 | env[*env_sizep] = NULL; |
|
920 | 920 | } |
|
921 | 921 | |
|
922 | 922 | // What is the new value? |
|
923 | 923 | char *new; |
|
924 | 924 | if (r->val) |
|
925 | 925 | { |
|
926 | 926 | if (!r->val[0]) |
|
927 | 927 | return; |
|
928 | 928 | new = xmalloc(r->var_len + 1 + strlen(r->val) + 1); |
|
929 | 929 | sprintf(new, "%s=%s", r->var, r->val); |
|
930 | 930 | } |
|
931 | 931 | else |
|
932 | 932 | { |
|
933 | 933 | pos = 0; |
|
934 | 934 | while (environ[pos] && !match_env_var(environ[pos], r)) |
|
935 | 935 | pos++; |
|
936 | 936 | if (!(new = environ[pos])) |
|
937 | 937 | return; |
|
938 | 938 | } |
|
939 | 939 | |
|
940 | 940 | // Add it at the end of the array |
|
941 | 941 | env[(*env_sizep)++] = new; |
|
942 | 942 | env[*env_sizep] = NULL; |
|
943 | 943 | } |
|
944 | 944 | |
|
945 | 945 | static char ** |
|
946 | 946 | setup_environment(void) |
|
947 | 947 | { |
|
948 | 948 | // Link built-in rules with user rules |
|
949 | 949 | for (int i=ARRAY_SIZE(default_env_rules)-1; i >= 0; i--) |
|
950 | 950 | { |
|
951 | 951 | default_env_rules[i].next = first_env_rule; |
|
952 | 952 | first_env_rule = &default_env_rules[i]; |
|
953 | 953 | } |
|
954 | 954 | |
|
955 | 955 | // Scan the original environment |
|
956 | 956 | char **orig_env = environ; |
|
957 | 957 | int orig_size = 0; |
|
958 | 958 | while (orig_env[orig_size]) |
|
959 | 959 | orig_size++; |
|
960 | 960 | |
|
961 | 961 | // For each rule, reserve one more slot and calculate length |
|
962 | 962 | int num_rules = 0; |
|
963 | 963 | for (struct env_rule *r = first_env_rule; r; r=r->next) |
|
964 | 964 | { |
|
965 | 965 | num_rules++; |
|
966 | 966 | r->var_len = strlen(r->var); |
|
967 | 967 | } |
|
968 | 968 | |
|
969 | 969 | // Create a new environment |
|
970 | 970 | char **env = xmalloc((orig_size + num_rules + 1) * sizeof(char *)); |
|
971 | 971 | int size; |
|
972 | 972 | if (pass_environ) |
|
973 | 973 | { |
|
974 | 974 | memcpy(env, environ, orig_size * sizeof(char *)); |
|
975 | 975 | size = orig_size; |
|
976 | 976 | } |
|
977 | 977 | else |
|
978 | 978 | size = 0; |
|
979 | 979 | env[size] = NULL; |
|
980 | 980 | |
|
981 | 981 | // Apply the rules one by one |
|
982 | 982 | for (struct env_rule *r = first_env_rule; r; r=r->next) |
|
983 | 983 | apply_env_rule(env, &size, r); |
|
984 | 984 | |
|
985 | 985 | // Return the new env and pass some gossip |
|
986 | 986 | if (verbose > 1) |
|
987 | 987 | { |
|
988 | 988 | fprintf(stderr, "Passing environment:\n"); |
|
989 | 989 | for (int i=0; env[i]; i++) |
|
990 | 990 | fprintf(stderr, "\t%s\n", env[i]); |
|
991 | 991 | } |
|
992 | 992 | return env; |
|
993 | 993 | } |
|
994 | 994 | |
|
995 | 995 | /*** Low-level parsing of syscalls ***/ |
|
996 | 996 | |
|
997 | 997 | #ifdef CONFIG_BOX_KERNEL_AMD64 |
|
998 | 998 | typedef uint64_t arg_t; |
|
999 | 999 | #else |
|
1000 | 1000 | typedef uint32_t arg_t; |
|
1001 | 1001 | #endif |
|
1002 | 1002 | |
|
1003 | 1003 | struct syscall_args { |
|
1004 | 1004 | arg_t sys; |
|
1005 | 1005 | arg_t arg1, arg2, arg3; |
|
1006 | 1006 | arg_t result; |
|
1007 | 1007 | struct user user; |
|
1008 | 1008 | }; |
|
1009 | 1009 | |
|
1010 | 1010 | static int user_mem_fd; |
|
1011 | 1011 | |
|
1012 | 1012 | static int read_user_mem(arg_t addr, char *buf, int len) |
|
1013 | 1013 | { |
|
1014 | 1014 | if (!user_mem_fd) |
|
1015 | 1015 | { |
|
1016 | 1016 | char memname[64]; |
|
1017 | 1017 | sprintf(memname, "/proc/%d/mem", (int) box_pid); |
|
1018 | 1018 | user_mem_fd = open(memname, O_RDONLY); |
|
1019 | 1019 | if (user_mem_fd < 0) |
|
1020 | 1020 | die("open(%s): %m", memname); |
|
1021 | 1021 | } |
|
1022 | 1022 | if (lseek64(user_mem_fd, addr, SEEK_SET) < 0) |
|
1023 | 1023 | die("lseek64(mem): %m"); |
|
1024 | 1024 | return read(user_mem_fd, buf, len); |
|
1025 | 1025 | } |
|
1026 | 1026 | |
|
1027 | 1027 | static void close_user_mem(void) |
|
1028 | 1028 | { |
|
1029 | 1029 | if (user_mem_fd) |
|
1030 | 1030 | { |
|
1031 | 1031 | close(user_mem_fd); |
|
1032 | 1032 | user_mem_fd = 0; |
|
1033 | 1033 | } |
|
1034 | 1034 | } |
|
1035 | 1035 | |
|
1036 | 1036 | #ifdef CONFIG_BOX_KERNEL_AMD64 |
|
1037 | 1037 | |
|
1038 | 1038 | static void |
|
1039 | 1039 | get_syscall_args(struct syscall_args *a, int is_exit) |
|
1040 | 1040 | { |
|
1041 | 1041 | if (ptrace(PTRACE_GETREGS, box_pid, NULL, &a->user) < 0) |
|
1042 | 1042 | die("ptrace(PTRACE_GETREGS): %m"); |
|
1043 | 1043 | a->sys = a->user.regs.orig_rax; |
|
1044 | 1044 | a->result = a->user.regs.rax; |
|
1045 | 1045 | |
|
1046 | 1046 | /* |
|
1047 | 1047 | * CAVEAT: We have to check carefully that this is a real 64-bit syscall. |
|
1048 | 1048 | * We test whether the process runs in 64-bit mode, but surprisingly this |
|
1049 | 1049 | * is not enough: a 64-bit process can still issue the INT 0x80 instruction |
|
1050 | 1050 | * which performs a 32-bit syscall. Currently, the only known way how to |
|
1051 | 1051 | * detect this situation is to inspect the instruction code (the kernel |
|
1052 | 1052 | * keeps a syscall type flag internally, but it is not accessible from |
|
1053 | 1053 | * user space). Hopefully, there is no instruction whose suffix is the |
|
1054 | 1054 | * code of the SYSCALL instruction. Sometimes, one would wish the |
|
1055 | 1055 | * instruction codes to be unique even when read backwards :) |
|
1056 | 1056 | */ |
|
1057 | 1057 | |
|
1058 | 1058 | if (is_exit) |
|
1059 | 1059 | return; |
|
1060 | 1060 | |
|
1061 | 1061 | int sys_type; |
|
1062 | 1062 | uint16_t instr; |
|
1063 | 1063 | |
|
1064 | 1064 | switch (a->user.regs.cs) |
|
1065 | 1065 | { |
|
1066 | 1066 | case 0x23: |
|
1067 | 1067 | // 32-bit CPU mode => only 32-bit syscalls can be issued |
|
1068 | 1068 | sys_type = 32; |
|
1069 | 1069 | break; |
|
1070 | 1070 | case 0x33: |
|
1071 | 1071 | // 64-bit CPU mode |
|
1072 | 1072 | if (read_user_mem(a->user.regs.rip-2, (char *) &instr, 2) != 2) |
|
1073 | 1073 | err("FO: Cannot read syscall instruction"); |
|
1074 | 1074 | switch (instr) |
|
1075 | 1075 | { |
|
1076 | 1076 | case 0x050f: |
|
1077 | 1077 | break; |
|
1078 | 1078 | case 0x80cd: |
|
1079 | 1079 | err("FO: Forbidden 32-bit syscall in 64-bit mode"); |
|
1080 | 1080 | default: |
|
1081 | 1081 | err("XX: Unknown syscall instruction %04x", instr); |
|
1082 | 1082 | } |
|
1083 | 1083 | sys_type = 64; |
|
1084 | 1084 | break; |
|
1085 | 1085 | default: |
|
1086 | 1086 | err("XX: Unknown code segment %04jx", (intmax_t) a->user.regs.cs); |
|
1087 | 1087 | } |
|
1088 | 1088 | |
|
1089 | 1089 | #ifdef CONFIG_BOX_USER_AMD64 |
|
1090 | 1090 | if (sys_type != 64) |
|
1091 | 1091 | err("FO: Forbidden %d-bit mode syscall", sys_type); |
|
1092 | 1092 | #else |
|
1093 | 1093 | if (sys_type != (exec_seen ? 32 : 64)) |
|
1094 | 1094 | err("FO: Forbidden %d-bit mode syscall", sys_type); |
|
1095 | 1095 | #endif |
|
1096 | 1096 | |
|
1097 | 1097 | if (sys_type == 32) |
|
1098 | 1098 | { |
|
1099 | 1099 | a->arg1 = a->user.regs.rbx; |
|
1100 | 1100 | a->arg2 = a->user.regs.rcx; |
|
1101 | 1101 | a->arg3 = a->user.regs.rdx; |
|
1102 | 1102 | } |
|
1103 | 1103 | else |
|
1104 | 1104 | { |
|
1105 | 1105 | a->arg1 = a->user.regs.rdi; |
|
1106 | 1106 | a->arg2 = a->user.regs.rsi; |
|
1107 | 1107 | a->arg3 = a->user.regs.rdx; |
|
1108 | 1108 | } |
|
1109 | 1109 | } |
|
1110 | 1110 | |
|
1111 | 1111 | static void |
|
1112 | 1112 | set_syscall_nr(struct syscall_args *a, arg_t sys) |
|
1113 | 1113 | { |
|
1114 | 1114 | a->sys = sys; |
|
1115 | 1115 | a->user.regs.orig_rax = sys; |
|
1116 | 1116 | if (ptrace(PTRACE_SETREGS, box_pid, NULL, &a->user) < 0) |
|
1117 | 1117 | die("ptrace(PTRACE_SETREGS): %m"); |
|
1118 | 1118 | } |
|
1119 | 1119 | |
|
1120 | 1120 | static void |
|
1121 | 1121 | sanity_check(void) |
|
1122 | 1122 | { |
|
1123 | 1123 | } |
|
1124 | 1124 | |
|
1125 | 1125 | #else |
|
1126 | 1126 | |
|
1127 | 1127 | static void |
|
1128 | 1128 | get_syscall_args(struct syscall_args *a, int is_exit UNUSED) |
|
1129 | 1129 | { |
|
1130 | 1130 | if (ptrace(PTRACE_GETREGS, box_pid, NULL, &a->user) < 0) |
|
1131 | 1131 | die("ptrace(PTRACE_GETREGS): %m"); |
|
1132 | 1132 | a->sys = a->user.regs.orig_eax; |
|
1133 | 1133 | a->arg1 = a->user.regs.ebx; |
|
1134 | 1134 | a->arg2 = a->user.regs.ecx; |
|
1135 | 1135 | a->arg3 = a->user.regs.edx; |
|
1136 | 1136 | a->result = a->user.regs.eax; |
|
1137 | 1137 | } |
|
1138 | 1138 | |
|
1139 | 1139 | static void |
|
1140 | 1140 | set_syscall_nr(struct syscall_args *a, arg_t sys) |
|
1141 | 1141 | { |
|
1142 | 1142 | a->sys = sys; |
|
1143 | 1143 | a->user.regs.orig_eax = sys; |
|
1144 | 1144 | if (ptrace(PTRACE_SETREGS, box_pid, NULL, &a->user) < 0) |
|
1145 | 1145 | die("ptrace(PTRACE_SETREGS): %m"); |
|
1146 | 1146 | } |
|
1147 | 1147 | |
|
1148 | 1148 | static void |
|
1149 | 1149 | sanity_check(void) |
|
1150 | 1150 | { |
|
1151 | 1151 | #if !defined(CONFIG_BOX_ALLOW_INSECURE) |
|
1152 | 1152 | struct utsname uts; |
|
1153 | 1153 | if (uname(&uts) < 0) |
|
1154 | 1154 | die("uname() failed: %m"); |
|
1155 | 1155 | |
|
1156 | 1156 | if (!strcmp(uts.machine, "x86_64")) |
|
1157 | 1157 | die("Running 32-bit sandbox on 64-bit kernels is inherently unsafe. Please get a 64-bit version."); |
|
1158 | 1158 | #endif |
|
1159 | 1159 | } |
|
1160 | 1160 | |
|
1161 | 1161 | #endif |
|
1162 | 1162 | |
|
1163 | 1163 | /*** Syscall checks ***/ |
|
1164 | 1164 | |
|
1165 | 1165 | static void |
|
1166 | 1166 | valid_filename(arg_t addr) |
|
1167 | 1167 | { |
|
1168 | 1168 | char namebuf[4096], *p, *end; |
|
1169 | 1169 | |
|
1170 | 1170 | if (!file_access) |
|
1171 | 1171 | err("FA: File access forbidden"); |
|
1172 | 1172 | if (file_access >= 9) |
|
1173 | 1173 | return; |
|
1174 | 1174 | |
|
1175 | 1175 | p = end = namebuf; |
|
1176 | 1176 | do |
|
1177 | 1177 | { |
|
1178 | 1178 | if (p >= end) |
|
1179 | 1179 | { |
|
1180 | 1180 | int remains = PAGE_SIZE - (addr & (PAGE_SIZE-1)); |
|
1181 | 1181 | int l = namebuf + sizeof(namebuf) - end; |
|
1182 | 1182 | if (l > remains) |
|
1183 | 1183 | l = remains; |
|
1184 | 1184 | if (!l) |
|
1185 | 1185 | err("FA: Access to file with name too long"); |
|
1186 | 1186 | remains = read_user_mem(addr, end, l); |
|
1187 | 1187 | if (remains < 0) |
|
1188 | 1188 | die("read(mem): %m"); |
|
1189 | 1189 | if (!remains) |
|
1190 | 1190 | err("FA: Access to file with name out of memory"); |
|
1191 | 1191 | end += remains; |
|
1192 | 1192 | addr += remains; |
|
1193 | 1193 | } |
|
1194 | 1194 | } |
|
1195 | 1195 | while (*p++); |
|
1196 | 1196 | |
|
1197 | 1197 | msg("[%s] ", namebuf); |
|
1198 | 1198 | if (file_access >= 3) |
|
1199 | 1199 | return; |
|
1200 | 1200 | |
|
1201 | 1201 | // Everything in current directory is permitted |
|
1202 | 1202 | if (!strchr(namebuf, '/') && strcmp(namebuf, "..")) |
|
1203 | 1203 | return; |
|
1204 | 1204 | |
|
1205 | 1205 | // ".." anywhere in the path is forbidden |
|
1206 | 1206 | enum action act = A_DEFAULT; |
|
1207 | 1207 | if (strstr(namebuf, "..")) |
|
1208 | 1208 | act = A_NO; |
|
1209 | 1209 | |
|
1210 | 1210 | // Scan user rules |
|
1211 | 1211 | for (struct path_rule *r = user_path_rules; r && !act; r=r->next) |
|
1212 | 1212 | act = match_path_rule(r, namebuf); |
|
1213 | 1213 | |
|
1214 | 1214 | // Scan built-in rules |
|
1215 | 1215 | if (file_access >= 2) |
|
1216 | 1216 | for (int i=0; i<ARRAY_SIZE(default_path_rules) && !act; i++) |
|
1217 | 1217 | act = match_path_rule(&default_path_rules[i], namebuf); |
|
1218 | 1218 | |
|
1219 | 1219 | if (act != A_YES) |
|
1220 | 1220 | err("FA: Forbidden access to file `%s'", namebuf); |
|
1221 | 1221 | } |
|
1222 | 1222 | |
|
1223 | 1223 | // Check syscall. If invalid, return -1, otherwise return the action mask. |
|
1224 | 1224 | static int |
|
1225 | 1225 | valid_syscall(struct syscall_args *a) |
|
1226 | 1226 | { |
|
1227 | 1227 | unsigned int sys = a->sys; |
|
1228 | 1228 | unsigned int act = (sys < NUM_ACTIONS) ? syscall_action[sys] : A_DEFAULT; |
|
1229 | 1229 | |
|
1230 | 1230 | if (act & A_LIBERAL) |
|
1231 | 1231 | { |
|
1232 | 1232 | if (filter_syscalls != 1) |
|
1233 | 1233 | act = A_DEFAULT; |
|
1234 | 1234 | } |
|
1235 | 1235 | |
|
1236 | 1236 | switch (act & A_ACTION_MASK) |
|
1237 | 1237 | { |
|
1238 | 1238 | case A_YES: |
|
1239 | 1239 | return act; |
|
1240 | 1240 | case A_NO: |
|
1241 | 1241 | return -1; |
|
1242 | 1242 | case A_FILENAME: |
|
1243 | 1243 | valid_filename(a->arg1); |
|
1244 | 1244 | return act; |
|
1245 | 1245 | default: ; |
|
1246 | 1246 | } |
|
1247 | 1247 | |
|
1248 | 1248 | switch (sys) |
|
1249 | 1249 | { |
|
1250 | 1250 | case __NR_kill: |
|
1251 | 1251 | if (a->arg1 == (arg_t) box_pid) |
|
1252 | 1252 | { |
|
1253 | 1253 | meta_printf("exitsig:%d\n", (int) a->arg2); |
|
1254 | 1254 | err("SG: Committed suicide by signal %d", (int) a->arg2); |
|
1255 | 1255 | } |
|
1256 | 1256 | return -1; |
|
1257 | 1257 | case __NR_tgkill: |
|
1258 | 1258 | if (a->arg1 == (arg_t) box_pid && a->arg2 == (arg_t) box_pid) |
|
1259 | 1259 | { |
|
1260 | 1260 | meta_printf("exitsig:%d\n", (int) a->arg3); |
|
1261 | 1261 | err("SG: Committed suicide by signal %d", (int) a->arg3); |
|
1262 | 1262 | } |
|
1263 | 1263 | return -1; |
|
1264 | 1264 | default: |
|
1265 | 1265 | return -1; |
|
1266 | 1266 | } |
|
1267 | 1267 | } |
|
1268 | 1268 | |
|
1269 | 1269 | static void |
|
1270 | 1270 | signal_alarm(int unused UNUSED) |
|
1271 | 1271 | { |
|
1272 | 1272 | /* Time limit checks are synchronous, so we only schedule them there. */ |
|
1273 | 1273 | timer_tick = 1; |
|
1274 | 1274 | alarm(1); |
|
1275 | 1275 | } |
|
1276 | 1276 | |
|
1277 | 1277 | static void |
|
1278 | 1278 | signal_int(int unused UNUSED) |
|
1279 | 1279 | { |
|
1280 | 1280 | /* Interrupts are fatal, so no synchronization requirements. */ |
|
1281 | 1281 | meta_printf("exitsig:%d\n", SIGINT); |
|
1282 | 1282 | err("SG: Interrupted"); |
|
1283 | 1283 | } |
|
1284 | 1284 | |
|
1285 | 1285 | #define PROC_BUF_SIZE 4096 |
|
1286 | 1286 | static void |
|
1287 | 1287 | read_proc_file(char *buf, char *name, int *fdp) |
|
1288 | 1288 | { |
|
1289 | 1289 | int c; |
|
1290 | 1290 | |
|
1291 | 1291 | if (!*fdp) |
|
1292 | 1292 | { |
|
1293 | 1293 | sprintf(buf, "/proc/%d/%s", (int) box_pid, name); |
|
1294 | 1294 | *fdp = open(buf, O_RDONLY); |
|
1295 | 1295 | if (*fdp < 0) |
|
1296 | 1296 | die("open(%s): %m", buf); |
|
1297 | 1297 | } |
|
1298 | 1298 | lseek(*fdp, 0, SEEK_SET); |
|
1299 | 1299 | if ((c = read(*fdp, buf, PROC_BUF_SIZE-1)) < 0) |
|
1300 | 1300 | die("read on /proc/$pid/%s: %m", name); |
|
1301 | 1301 | if (c >= PROC_BUF_SIZE-1) |
|
1302 | 1302 | die("/proc/$pid/%s too long", name); |
|
1303 | 1303 | buf[c] = 0; |
|
1304 | 1304 | } |
|
1305 | 1305 | |
|
1306 | 1306 | static void |
|
1307 | 1307 | check_timeout(void) |
|
1308 | 1308 | { |
|
1309 | 1309 | if (wall_timeout) |
|
1310 | 1310 | { |
|
1311 | 1311 | struct timeval now, wall; |
|
1312 | 1312 | int wall_ms; |
|
1313 | 1313 | gettimeofday(&now, NULL); |
|
1314 | 1314 | timersub(&now, &start_time, &wall); |
|
1315 | 1315 | wall_ms = wall.tv_sec*1000 + wall.tv_usec/1000; |
|
1316 | 1316 | if (wall_ms > wall_timeout) |
|
1317 | 1317 | err("TO: Time limit exceeded (wall clock)"); |
|
1318 | 1318 | if (verbose > 1) |
|
1319 | 1319 | fprintf(stderr, "[wall time check: %d msec]\n", wall_ms); |
|
1320 | 1320 | } |
|
1321 | 1321 | if (timeout) |
|
1322 | 1322 | { |
|
1323 | 1323 | char buf[PROC_BUF_SIZE], *x; |
|
1324 | 1324 | int utime, stime, ms; |
|
1325 | 1325 | static int proc_stat_fd; |
|
1326 | 1326 | read_proc_file(buf, "stat", &proc_stat_fd); |
|
1327 | 1327 | x = buf; |
|
1328 | 1328 | while (*x && *x != ' ') |
|
1329 | 1329 | x++; |
|
1330 | 1330 | while (*x == ' ') |
|
1331 | 1331 | x++; |
|
1332 | 1332 | if (*x++ != '(') |
|
1333 | 1333 | die("proc stat syntax error 1"); |
|
1334 | 1334 | while (*x && (*x != ')' || x[1] != ' ')) |
|
1335 | 1335 | x++; |
|
1336 | 1336 | while (*x == ')' || *x == ' ') |
|
1337 | 1337 | x++; |
|
1338 | 1338 | if (sscanf(x, "%*c %*d %*d %*d %*d %*d %*d %*d %*d %*d %*d %d %d", &utime, &stime) != 2) |
|
1339 | 1339 | die("proc stat syntax error 2"); |
|
1340 | 1340 | ms = (utime + stime) * 1000 / ticks_per_sec; |
|
1341 | 1341 | if (verbose > 1) |
|
1342 | 1342 | fprintf(stderr, "[time check: %d msec]\n", ms); |
|
1343 | 1343 | if (ms > timeout && ms > extra_timeout) |
|
1344 | 1344 | err("TO: Time limit exceeded"); |
|
1345 | 1345 | } |
|
1346 | 1346 | } |
|
1347 | 1347 | |
|
1348 | 1348 | static void |
|
1349 | 1349 | sample_mem_peak(void) |
|
1350 | 1350 | { |
|
1351 | 1351 | /* |
|
1352 | 1352 | * We want to find out the peak memory usage of the process, which is |
|
1353 | 1353 | * maintained by the kernel, but unforunately it gets lost when the |
|
1354 | 1354 | * process exits (it is not reported in struct rusage). Therefore we |
|
1355 | 1355 | * have to sample it whenever we suspect that the process is about |
|
1356 | 1356 | * to exit. |
|
1357 | 1357 | */ |
|
1358 | 1358 | char buf[PROC_BUF_SIZE], *x; |
|
1359 | 1359 | static int proc_status_fd; |
|
1360 | 1360 | read_proc_file(buf, "status", &proc_status_fd); |
|
1361 | 1361 | |
|
1362 | 1362 | x = buf; |
|
1363 | 1363 | while (*x) |
|
1364 | 1364 | { |
|
1365 | 1365 | char *key = x; |
|
1366 | 1366 | while (*x && *x != ':' && *x != '\n') |
|
1367 | 1367 | x++; |
|
1368 | 1368 | if (!*x || *x == '\n') |
|
1369 | 1369 | break; |
|
1370 | 1370 | *x++ = 0; |
|
1371 | 1371 | while (*x == ' ' || *x == '\t') |
|
1372 | 1372 | x++; |
|
1373 | 1373 | |
|
1374 | 1374 | char *val = x; |
|
1375 | 1375 | while (*x && *x != '\n') |
|
1376 | 1376 | x++; |
|
1377 | 1377 | if (!*x) |
|
1378 | 1378 | break; |
|
1379 | 1379 | *x++ = 0; |
|
1380 | 1380 | |
|
1381 | 1381 | if (!strcmp(key, "VmPeak")) |
|
1382 | 1382 | { |
|
1383 | 1383 | int peak = atoi(val); |
|
1384 | 1384 | if (peak > mem_peak_kb) |
|
1385 | 1385 | mem_peak_kb = peak; |
|
1386 | 1386 | } |
|
1387 | 1387 | } |
|
1388 | 1388 | |
|
1389 | 1389 | if (verbose > 1) |
|
1390 | 1390 | msg("[mem-peak: %u KB]\n", mem_peak_kb); |
|
1391 | 1391 | } |
|
1392 | 1392 | |
|
1393 | 1393 | static void |
|
1394 | 1394 | boxkeeper(void) |
|
1395 | 1395 | { |
|
1396 | 1396 | int syscall_count = (filter_syscalls ? 0 : 1); |
|
1397 | 1397 | struct sigaction sa; |
|
1398 | 1398 | |
|
1399 | 1399 | is_ptraced = 1; |
|
1400 | 1400 | |
|
1401 | 1401 | bzero(&sa, sizeof(sa)); |
|
1402 | 1402 | sa.sa_handler = signal_int; |
|
1403 | 1403 | sigaction(SIGINT, &sa, NULL); |
|
1404 | 1404 | |
|
1405 | 1405 | gettimeofday(&start_time, NULL); |
|
1406 | 1406 | ticks_per_sec = sysconf(_SC_CLK_TCK); |
|
1407 | 1407 | if (ticks_per_sec <= 0) |
|
1408 | 1408 | die("Invalid ticks_per_sec!"); |
|
1409 | 1409 | |
|
1410 | 1410 | if (timeout || wall_timeout) |
|
1411 | 1411 | { |
|
1412 | 1412 | sa.sa_handler = signal_alarm; |
|
1413 | 1413 | sigaction(SIGALRM, &sa, NULL); |
|
1414 | 1414 | alarm(1); |
|
1415 | 1415 | } |
|
1416 | 1416 | |
|
1417 | 1417 | for(;;) |
|
1418 | 1418 | { |
|
1419 | 1419 | struct rusage rus; |
|
1420 | 1420 | int stat; |
|
1421 | 1421 | pid_t p; |
|
1422 | 1422 | if (timer_tick) |
|
1423 | 1423 | { |
|
1424 | 1424 | check_timeout(); |
|
1425 | 1425 | timer_tick = 0; |
|
1426 | 1426 | } |
|
1427 | 1427 | p = wait4(box_pid, &stat, WUNTRACED, &rus); |
|
1428 | 1428 | if (p < 0) |
|
1429 | 1429 | { |
|
1430 | 1430 | if (errno == EINTR) |
|
1431 | 1431 | continue; |
|
1432 | 1432 | die("wait4: %m"); |
|
1433 | 1433 | } |
|
1434 | 1434 | if (p != box_pid) |
|
1435 | 1435 | die("wait4: unknown pid %d exited!", p); |
|
1436 | 1436 | if (WIFEXITED(stat)) |
|
1437 | 1437 | { |
|
1438 | 1438 | box_pid = 0; |
|
1439 | 1439 | final_stats(&rus); |
|
1440 | 1440 | if (WEXITSTATUS(stat)) |
|
1441 | 1441 | { |
|
1442 | 1442 | if (syscall_count) |
|
1443 | 1443 | { |
|
1444 | 1444 | meta_printf("exitcode:%d\n", WEXITSTATUS(stat)); |
|
1445 | 1445 | err("RE: Exited with error status %d", WEXITSTATUS(stat)); |
|
1446 | 1446 | } |
|
1447 | 1447 | else |
|
1448 | 1448 | { |
|
1449 | 1449 | // Internal error happened inside the child process and it has been already reported. |
|
1450 | 1450 | box_exit(2); |
|
1451 | 1451 | } |
|
1452 | 1452 | } |
|
1453 | 1453 | if (timeout && total_ms > timeout) |
|
1454 | 1454 | err("TO: Time limit exceeded"); |
|
1455 | 1455 | if (wall_timeout && wall_ms > wall_timeout) |
|
1456 | 1456 | err("TO: Time limit exceeded (wall clock)"); |
|
1457 | 1457 | flush_line(); |
|
1458 | 1458 | fprintf(stderr,"OK\n"); |
|
1459 | 1459 | box_exit(0); |
|
1460 | 1460 | } |
|
1461 | 1461 | if (WIFSIGNALED(stat)) |
|
1462 | 1462 | { |
|
1463 | 1463 | box_pid = 0; |
|
1464 | 1464 | meta_printf("exitsig:%d\n", WTERMSIG(stat)); |
|
1465 | 1465 | final_stats(&rus); |
|
1466 | 1466 | err("SG: Caught fatal signal %d%s", WTERMSIG(stat), (syscall_count ? "" : " during startup")); |
|
1467 | 1467 | } |
|
1468 | 1468 | if (WIFSTOPPED(stat)) |
|
1469 | 1469 | { |
|
1470 | 1470 | int sig = WSTOPSIG(stat); |
|
1471 | 1471 | if (sig == SIGTRAP) |
|
1472 | 1472 | { |
|
1473 | 1473 | if (verbose > 2) |
|
1474 | 1474 | msg("[ptrace status %08x] ", stat); |
|
1475 | 1475 | static int stop_count; |
|
1476 | 1476 | if (!stop_count++) /* Traceme request */ |
|
1477 | 1477 | msg(">> Traceme request caught\n"); |
|
1478 | 1478 | else |
|
1479 | 1479 | err("SG: Breakpoint"); |
|
1480 | 1480 | ptrace(PTRACE_SYSCALL, box_pid, 0, 0); |
|
1481 | 1481 | } |
|
1482 | 1482 | else if (sig == (SIGTRAP | 0x80)) |
|
1483 | 1483 | { |
|
1484 | 1484 | if (verbose > 2) |
|
1485 | 1485 | msg("[ptrace status %08x] ", stat); |
|
1486 | 1486 | struct syscall_args a; |
|
1487 | 1487 | static unsigned int sys_tick, last_act; |
|
1488 | 1488 | static arg_t last_sys; |
|
1489 | 1489 | if (++sys_tick & 1) /* Syscall entry */ |
|
1490 | 1490 | { |
|
1491 | 1491 | char namebuf[32]; |
|
1492 | 1492 | int act; |
|
1493 | 1493 | |
|
1494 | 1494 | get_syscall_args(&a, 0); |
|
1495 | 1495 | arg_t sys = a.sys; |
|
1496 | 1496 | msg(">> Syscall %-12s (%08jx,%08jx,%08jx) ", syscall_name(sys, namebuf), (intmax_t) a.arg1, (intmax_t) a.arg2, (intmax_t) a.arg3); |
|
1497 | 1497 | if (!exec_seen) |
|
1498 | 1498 | { |
|
1499 | 1499 | msg("[master] "); |
|
1500 | 1500 | if (sys == NATIVE_NR_execve) |
|
1501 | 1501 | { |
|
1502 | 1502 | exec_seen = 1; |
|
1503 | 1503 | close_user_mem(); |
|
1504 | 1504 | } |
|
1505 | 1505 | } |
|
1506 | 1506 | else if ((act = valid_syscall(&a)) >= 0) |
|
1507 | 1507 | { |
|
1508 | 1508 | last_act = act; |
|
1509 | 1509 | syscall_count++; |
|
1510 | 1510 | if (act & A_SAMPLE_MEM) |
|
1511 | 1511 | sample_mem_peak(); |
|
1512 | 1512 | } |
|
1513 | 1513 | else |
|
1514 | 1514 | { |
|
1515 | 1515 | /* |
|
1516 | 1516 | * Unfortunately, PTRACE_KILL kills _after_ the syscall completes, |
|
1517 | 1517 | * so we have to change it to something harmless (e.g., an undefined |
|
1518 | 1518 | * syscall) and make the program continue. |
|
1519 | 1519 | */ |
|
1520 | 1520 | set_syscall_nr(&a, ~(arg_t)0); |
|
1521 | 1521 | err("FO: Forbidden syscall %s", syscall_name(sys, namebuf)); |
|
1522 | 1522 | } |
|
1523 | 1523 | last_sys = sys; |
|
1524 | 1524 | } |
|
1525 | 1525 | else /* Syscall return */ |
|
1526 | 1526 | { |
|
1527 | 1527 | get_syscall_args(&a, 1); |
|
1528 | 1528 | if (a.sys == ~(arg_t)0) |
|
1529 | 1529 | { |
|
1530 | 1530 | /* Some syscalls (sigreturn et al.) do not return a value */ |
|
1531 | 1531 | if (!(last_act & A_NO_RETVAL)) |
|
1532 | 1532 | err("XX: Syscall does not return, but it should"); |
|
1533 | 1533 | } |
|
1534 | 1534 | else |
|
1535 | 1535 | { |
|
1536 | 1536 | if (a.sys != last_sys) |
|
1537 | 1537 | err("XX: Mismatched syscall entry/exit"); |
|
1538 | 1538 | } |
|
1539 | 1539 | if (last_act & A_NO_RETVAL) |
|
1540 | 1540 | msg("= ?\n"); |
|
1541 | 1541 | else |
|
1542 | 1542 | msg("= %jd\n", (intmax_t) a.result); |
|
1543 | 1543 | } |
|
1544 | 1544 | ptrace(PTRACE_SYSCALL, box_pid, 0, 0); |
|
1545 | 1545 | } |
|
1546 | 1546 | else if (sig == SIGSTOP) |
|
1547 | 1547 | { |
|
1548 | 1548 | msg(">> SIGSTOP\n"); |
|
1549 | 1549 | if (ptrace(PTRACE_SETOPTIONS, box_pid, NULL, (void *) PTRACE_O_TRACESYSGOOD) < 0) |
|
1550 | 1550 | die("ptrace(PTRACE_SETOPTIONS): %m"); |
|
1551 | 1551 | ptrace(PTRACE_SYSCALL, box_pid, 0, 0); |
|
1552 | 1552 | } |
|
1553 | 1553 | else if (sig != SIGXCPU && sig != SIGXFSZ) |
|
1554 | 1554 | { |
|
1555 | 1555 | msg(">> Signal %d\n", sig); |
|
1556 | 1556 | sample_mem_peak(); /* Signal might be fatal, so update mem-peak */ |
|
1557 | 1557 | ptrace(PTRACE_SYSCALL, box_pid, 0, sig); |
|
1558 | 1558 | } |
|
1559 | 1559 | else |
|
1560 | 1560 | { |
|
1561 | 1561 | meta_printf("exitsig:%d", sig); |
|
1562 | 1562 | err("SG: Received signal %d", sig); |
|
1563 | 1563 | } |
|
1564 | 1564 | } |
|
1565 | 1565 | else |
|
1566 | 1566 | die("wait4: unknown status %x, giving up!", stat); |
|
1567 | 1567 | } |
|
1568 | 1568 | } |
|
1569 | 1569 | |
|
1570 | 1570 | static void |
|
1571 | 1571 | box_inside(int argc, char **argv) |
|
1572 | 1572 | { |
|
1573 | 1573 | struct rlimit rl; |
|
1574 | 1574 | char *args[argc+1]; |
|
1575 | 1575 | |
|
1576 | 1576 | memcpy(args, argv, argc * sizeof(char *)); |
|
1577 | 1577 | args[argc] = NULL; |
|
1578 | 1578 | if (set_cwd && chdir(set_cwd)) |
|
1579 | 1579 | die("chdir: %m"); |
|
1580 | 1580 | if (redir_stdin) |
|
1581 | 1581 | { |
|
1582 | 1582 | close(0); |
|
1583 | 1583 | if (open(redir_stdin, O_RDONLY) != 0) |
|
1584 | 1584 | die("open(\"%s\"): %m", redir_stdin); |
|
1585 | 1585 | } |
|
1586 | 1586 | if (redir_stdout) |
|
1587 | 1587 | { |
|
1588 | 1588 | close(1); |
|
1589 | 1589 | if (open(redir_stdout, O_WRONLY | O_CREAT | O_TRUNC, 0666) != 1) |
|
1590 | 1590 | die("open(\"%s\"): %m", redir_stdout); |
|
1591 | 1591 | } |
|
1592 | 1592 | if (redir_stderr) |
|
1593 | 1593 | { |
|
1594 | 1594 | close(2); |
|
1595 | 1595 | if (open(redir_stderr, O_WRONLY | O_CREAT | O_TRUNC, 0666) != 2) |
|
1596 | 1596 | die("open(\"%s\"): %m", redir_stderr); |
|
1597 | 1597 | } |
|
1598 | 1598 | else |
|
1599 | 1599 | dup2(1, 2); |
|
1600 | 1600 | setpgrp(); |
|
1601 | 1601 | |
|
1602 | 1602 | if (memory_limit) |
|
1603 | 1603 | { |
|
1604 | 1604 | rl.rlim_cur = rl.rlim_max = memory_limit * 1024; |
|
1605 | 1605 | if (setrlimit(RLIMIT_AS, &rl) < 0) |
|
1606 | 1606 | die("setrlimit(RLIMIT_AS): %m"); |
|
1607 | 1607 | } |
|
1608 | 1608 | |
|
1609 | 1609 | rl.rlim_cur = rl.rlim_max = (stack_limit ? (rlim_t)stack_limit * 1024 : RLIM_INFINITY); |
|
1610 | 1610 | if (setrlimit(RLIMIT_STACK, &rl) < 0) |
|
1611 | 1611 | die("setrlimit(RLIMIT_STACK): %m"); |
|
1612 | 1612 | |
|
1613 | 1613 | rl.rlim_cur = rl.rlim_max = 64; |
|
1614 | 1614 | if (setrlimit(RLIMIT_NOFILE, &rl) < 0) |
|
1615 | 1615 | die("setrlimit(RLIMIT_NOFILE): %m"); |
|
1616 | 1616 | |
|
1617 | 1617 | char **env = setup_environment(); |
|
1618 | 1618 | if (filter_syscalls) |
|
1619 | 1619 | { |
|
1620 | 1620 | if (ptrace(PTRACE_TRACEME) < 0) |
|
1621 | 1621 | die("ptrace(PTRACE_TRACEME): %m"); |
|
1622 | 1622 | /* Trick: Make sure that we are stopped until the boxkeeper wakes up. */ |
|
1623 | 1623 | raise(SIGSTOP); |
|
1624 | 1624 | } |
|
1625 | 1625 | execve(args[0], args, env); |
|
1626 | 1626 | die("execve(\"%s\"): %m", args[0]); |
|
1627 | 1627 | } |
|
1628 | 1628 | |
|
1629 | 1629 | static void |
|
1630 | 1630 | usage(void) |
|
1631 | 1631 | { |
|
1632 | 1632 | fprintf(stderr, "Invalid arguments!\n"); |
|
1633 | 1633 | printf("\ |
|
1634 | 1634 | Usage: box [<options>] -- <command> <arguments>\n\ |
|
1635 | 1635 | \n\ |
|
1636 | 1636 | Options:\n\ |
|
1637 | 1637 | -a <level>\tSet file access level (0=none, 1=cwd, 2=/etc,/lib,..., 3=whole fs, 9=no checks; needs -f)\n\ |
|
1638 | 1638 | -c <dir>\tChange directory to <dir> first\n\ |
|
1639 | 1639 | -e\t\tInherit full environment of the parent process\n\ |
|
1640 | 1640 | -E <var>\tInherit the environment variable <var> from the parent process\n\ |
|
1641 | 1641 | -E <var>=<val>\tSet the environment variable <var> to <val>; unset it if <var> is empty\n\ |
|
1642 | 1642 | -f\t\tFilter system calls (-ff=very restricted)\n\ |
|
1643 | 1643 | -i <file>\tRedirect stdin from <file>\n\ |
|
1644 | 1644 | -k <size>\tLimit stack size to <size> KB (default: 0=unlimited)\n\ |
|
1645 | 1645 | -m <size>\tLimit address space to <size> KB\n\ |
|
1646 | 1646 | -M <file>\tOutput process information to <file> (name:value)\n\ |
|
1647 | 1647 | -o <file>\tRedirect stdout to <file>\n\ |
|
1648 | 1648 | -p <path>\tPermit access to the specified path (or subtree if it ends with a `/')\n\ |
|
1649 | 1649 | -p <path>=<act>\tDefine action for the specified path (<act>=yes/no)\n\ |
|
1650 | 1650 | -r <file>\tRedirect stderr to <file>\n\ |
|
1651 | 1651 | -s <sys>\tPermit the specified syscall (be careful)\n\ |
|
1652 | 1652 | -s <sys>=<act>\tDefine action for the specified syscall (<act>=yes/no/file)\n\ |
|
1653 | 1653 | -t <time>\tSet run time limit (seconds, fractions allowed)\n\ |
|
1654 | 1654 | -T\t\tAllow syscalls for measuring run time\n\ |
|
1655 | 1655 | -v\t\tBe verbose (use multiple times for even more verbosity)\n\ |
|
1656 | 1656 | -w <time>\tSet wall clock time limit (seconds, fractions allowed)\n\ |
|
1657 | 1657 | -x <time>\tSet extra timeout, before which a timing-out program is not yet killed,\n\ |
|
1658 | 1658 | \t\tso that its real execution time is reported (seconds, fractions allowed)\n\ |
|
1659 | + -A <opt>\tPass <opt> as additional argument to the <command>\n\ | |
|
1660 | + \t\tBe noted that this option will be appended after <arguments> respectively\n\ | |
|
1659 | 1661 | "); |
|
1660 | 1662 | exit(2); |
|
1661 | 1663 | } |
|
1662 | 1664 | |
|
1663 | 1665 | int |
|
1664 | 1666 | main(int argc, char **argv) |
|
1665 | 1667 | { |
|
1666 | 1668 | int c; |
|
1667 | 1669 | uid_t uid; |
|
1670 | + char **prog_argv = xmalloc(sizeof(char*) * argc); | |
|
1671 | + int prog_argc = 0; | |
|
1668 | 1672 | |
|
1669 | - while ((c = getopt(argc, argv, "a:c:eE:fi:k:m:M:o:p:r:s:t:Tvw:x:")) >= 0) | |
|
1673 | + while ((c = getopt(argc, argv, "a:c:eE:fi:k:m:M:o:p:r:s:t:Tvw:x:A:")) >= 0) | |
|
1670 | 1674 | switch (c) |
|
1671 | 1675 | { |
|
1672 | 1676 | case 'a': |
|
1673 | 1677 | file_access = atol(optarg); |
|
1674 | 1678 | break; |
|
1675 | 1679 | case 'c': |
|
1676 | 1680 | set_cwd = optarg; |
|
1677 | 1681 | break; |
|
1678 | 1682 | case 'e': |
|
1679 | 1683 | pass_environ = 1; |
|
1680 | 1684 | break; |
|
1681 | 1685 | case 'E': |
|
1682 | 1686 | if (!set_env_action(optarg)) |
|
1683 | 1687 | usage(); |
|
1684 | 1688 | break; |
|
1685 | 1689 | case 'f': |
|
1686 | 1690 | filter_syscalls++; |
|
1687 | 1691 | break; |
|
1688 | 1692 | case 'k': |
|
1689 | 1693 | stack_limit = atol(optarg); |
|
1690 | 1694 | break; |
|
1691 | 1695 | case 'i': |
|
1692 | 1696 | redir_stdin = optarg; |
|
1693 | 1697 | break; |
|
1694 | 1698 | case 'm': |
|
1695 | 1699 | memory_limit = atol(optarg); |
|
1696 | 1700 | break; |
|
1697 | 1701 | case 'M': |
|
1698 | 1702 | meta_open(optarg); |
|
1699 | 1703 | break; |
|
1700 | 1704 | case 'o': |
|
1701 | 1705 | redir_stdout = optarg; |
|
1702 | 1706 | break; |
|
1703 | 1707 | case 'p': |
|
1704 | 1708 | if (!set_path_action(optarg)) |
|
1705 | 1709 | usage(); |
|
1706 | 1710 | break; |
|
1707 | 1711 | case 'r': |
|
1708 | 1712 | redir_stderr = optarg; |
|
1709 | 1713 | break; |
|
1710 | 1714 | case 's': |
|
1711 | 1715 | if (!set_syscall_action(optarg)) |
|
1712 | 1716 | usage(); |
|
1713 | 1717 | break; |
|
1714 | 1718 | case 't': |
|
1715 | 1719 | timeout = 1000*atof(optarg); |
|
1716 | 1720 | break; |
|
1717 | 1721 | case 'T': |
|
1718 | 1722 | syscall_action[__NR_times] = A_YES; |
|
1719 | 1723 | break; |
|
1720 | 1724 | case 'v': |
|
1721 | 1725 | verbose++; |
|
1722 | 1726 | break; |
|
1723 | 1727 | case 'w': |
|
1724 | 1728 | wall_timeout = 1000*atof(optarg); |
|
1725 | 1729 | break; |
|
1726 | 1730 | case 'x': |
|
1727 | 1731 | extra_timeout = 1000*atof(optarg); |
|
1732 | + case 'A': | |
|
1733 | + prog_argv[prog_argc++] = strdup(optarg); | |
|
1734 | + break; | |
|
1728 | 1735 | break; |
|
1729 | 1736 | default: |
|
1730 | 1737 | usage(); |
|
1731 | 1738 | } |
|
1732 | 1739 | if (optind >= argc) |
|
1733 | 1740 | usage(); |
|
1734 | 1741 | |
|
1735 | 1742 | sanity_check(); |
|
1736 | 1743 | uid = geteuid(); |
|
1737 | 1744 | if (setreuid(uid, uid) < 0) |
|
1738 | 1745 | die("setreuid: %m"); |
|
1739 | 1746 | box_pid = fork(); |
|
1740 | 1747 | if (box_pid < 0) |
|
1741 | 1748 | die("fork: %m"); |
|
1742 | - if (!box_pid) | |
|
1743 | - box_inside(argc-optind, argv+optind); | |
|
1744 | - else | |
|
1749 | + if (!box_pid) { | |
|
1750 | + int real_argc = prog_argc + argc - optind; | |
|
1751 | + char **real_argv = xmalloc(sizeof(char*) * (real_argc)); | |
|
1752 | + for (int i = 0;i < argc-optind;i++) | |
|
1753 | + real_argv[i] = strdup(argv[i+optind]); | |
|
1754 | + for (int i = 0;i < prog_argc;i++) | |
|
1755 | + real_argv[argc - optind + i] = strdup(prog_argv[i]); | |
|
1756 | + box_inside(real_argc, real_argv); | |
|
1757 | + } else | |
|
1745 | 1758 | boxkeeper(); |
|
1746 | 1759 | die("Internal error: fell over edge of the world"); |
|
1747 | 1760 | } |
You need to be logged in to leave comments.
Login now