Bugzilla – Attachment 3250 Details for
Bug 2468
Option to include external files to sshd_config
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Requests
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Include server side with re-exec passing the include list (master, 2019)
sshd_include.patch (text/plain), 19.39 KB, created by
Jakub Jelen
on 2019-03-06 01:52:57 AEDT
(
hide
)
Description:
Include server side with re-exec passing the include list (master, 2019)
Filename:
MIME Type:
Creator:
Jakub Jelen
Created:
2019-03-06 01:52:57 AEDT
Size:
19.39 KB
patch
obsolete
>commit 76cc18139c78ef43613f0ff57549050c467129d7 >Author: Jakub Jelen <jjelen@redhat.com> >Date: Tue Sep 6 13:22:54 2016 +0200 > > Include server config > > Signed-off-by: Jakub Jelen <jjelen@redhat.com> > >diff --git a/auth.c b/auth.c >index 332b6220..4b629f93 100644 >--- a/auth.c >+++ b/auth.c >@@ -79,6 +79,7 @@ > > /* import */ > extern ServerOptions options; >+extern struct include_list includes; > extern int use_privsep; > extern struct sshbuf *loginmsg; > extern struct passwd *privsep_pw; >@@ -571,7 +572,7 @@ getpwnamallow(struct ssh *ssh, const char *user) > > ci = get_connection_info(ssh, 1, options.use_dns); > ci->user = user; >- parse_server_match_config(&options, ci); >+ parse_server_match_config(&options, &includes, ci); > log_change_level(options.log_level); > process_permitopen(ssh, &options); > >diff --git a/regress/Makefile b/regress/Makefile >index 925edf71..54a3d7d9 100644 >--- a/regress/Makefile >+++ b/regress/Makefile >@@ -82,6 +82,7 @@ LTESTS= connect \ > principals-command \ > cert-file \ > cfginclude \ >+ servcfginclude \ > allow-deny-users \ > authinfo > >@@ -116,7 +117,7 @@ CLEANFILES= *.core actual agent-key.* authorized_keys_${USERNAME} \ > sftp-server.sh sftp.log ssh-log-wrapper.sh ssh.log \ > ssh_config ssh_config.* ssh_proxy ssh_proxy_bak \ > ssh_proxy_envpass sshd.log sshd_config sshd_config_minimal \ >- sshd_config.orig sshd_proxy sshd_proxy.* sshd_proxy_bak \ >+ sshd_config.* sshd_proxy sshd_proxy.* sshd_proxy_bak \ > sshd_proxy_orig t10.out t10.out.pub t12.out t12.out.pub \ > t2.out t3.out t6.out1 t6.out2 t7.out t7.out.pub \ > t8.out t8.out.pub t9.out t9.out.pub testdata \ >diff --git a/regress/servcfginclude.sh b/regress/servcfginclude.sh >new file mode 100644 >index 00000000..e6bd87f8 >--- /dev/null >+++ b/regress/servcfginclude.sh >@@ -0,0 +1,151 @@ >+# Placed in the Public Domain. >+ >+tid="server config include" >+ >+cat > $OBJ/sshd_config.i << _EOF >+HostKey $OBJ/host.ssh-ed25519 >+Match host a >+ Banner /aa >+ >+Match host b >+ Banner /bb >+ Include $OBJ/sshd_config.i.* >+ >+Match host c >+ Include $OBJ/sshd_config.i.* >+ Banner /cc >+ >+Match host m >+ Include $OBJ/sshd_config.i.* >+ >+Match Host d >+ Banner /dd >+ >+Match Host e >+ Banner /ee >+ Include $OBJ/sshd_config.i.* >+ >+Match Host f >+ Include $OBJ/sshd_config.i.* >+ Banner /ff >+ >+Match Host n >+ Include $OBJ/sshd_config.i.* >+_EOF >+ >+cat > $OBJ/sshd_config.i.0 << _EOF >+Match host xxxxxx >+_EOF >+ >+cat > $OBJ/sshd_config.i.1 << _EOF >+Match host a >+ Banner /aaa >+ >+Match host b >+ Banner /bbb >+ >+Match host c >+ Banner /ccc >+ >+Match Host d >+ Banner /ddd >+ >+Match Host e >+ Banner /eee >+ >+Match Host f >+ Banner /fff >+_EOF >+ >+cat > $OBJ/sshd_config.i.2 << _EOF >+Match host a >+ Banner /aaaa >+ >+Match host b >+ Banner /bbbb >+ >+Match host c >+ Banner /cccc >+ >+Match Host d >+ Banner /dddd >+ >+Match Host e >+ Banner /eeee >+ >+Match Host f >+ Banner /ffff >+ >+Match all >+ Banner /xxxx >+_EOF >+ >+trial() { >+ _host="$1" >+ _exp="$2" >+ trace "Testing the match with host=$_host" >+ ${REAL_SSHD} -f $OBJ/sshd_config.i -T -C "host=$_host,user=test,addr=127.0.0.1" > $OBJ/sshd_config.out || >+ fatal "ssh config parse failed" >+ _got=`grep -i '^banner ' $OBJ/sshd_config.out | awk '{print $2}'` >+ if test "x$_exp" != "x$_got" ; then >+ fail "host $_host include fail: expected $_exp got $_got" >+ fi >+} >+ >+trial a /aa >+trial b /bb >+trial c /ccc >+trial d /dd >+trial e /ee >+trial f /fff >+trial m /xxxx >+trial n /xxxx >+trial x none >+ >+# Prepare an included config with an error. >+ >+cat > $OBJ/sshd_config.i.3 << _EOF >+Banner xxxx >+ Junk >+_EOF >+ >+${REAL_SSHD} -f $OBJ/sshd_config.i -C "host=a,user=test,addr=127.0.0.1" 2>/dev/null && \ >+ fail "sshd include allowed invalid config" >+ >+${REAL_SSHD} -f $OBJ/sshd_config.i -C "host=x,user=test,addr=127.0.0.1" 2>/dev/null && \ >+ fail "sshd include allowed invalid config" >+ >+rm -f $OBJ/sshd_config.i.* >+ >+# Ensure that a missing include is not fatal. >+cat > $OBJ/sshd_config.i << _EOF >+HostKey $OBJ/host.ssh-ed25519 >+Include $OBJ/sshd_config.i.* >+Banner /aa >+_EOF >+ >+trial a /aa >+ >+# Ensure that Match/Host in an included config does not affect parent. >+cat > $OBJ/sshd_config.i.x << _EOF >+Match host x >+_EOF >+ >+trial a /aa >+ >+cat > $OBJ/sshd_config.i.x << _EOF >+Match Host x >+_EOF >+ >+trial a /aa >+ >+# Ensure the empty include directive is not accepted >+cat > $OBJ/sshd_config.i.x << _EOF >+Include >+_EOF >+ >+${REAL_SSHD} -f $OBJ/sshd_config.i.x -C "host=x,user=test,addr=127.0.0.1" 2>/dev/null && \ >+ fail "sshd allowed empty Include option" >+ >+# cleanup >+rm -f $OBJ/sshd_config.i $OBJ/sshd_config.i.* $OBJ/sshd_config.out >diff --git a/regress/test-exec.sh b/regress/test-exec.sh >index e8379e17..83a2ca17 100644 >--- a/regress/test-exec.sh >+++ b/regress/test-exec.sh >@@ -224,6 +224,7 @@ echo "exec ${SSH} -E${TEST_SSH_LOGFILE} "'"$@"' >>$SSHLOGWRAP > > chmod a+rx $OBJ/ssh-log-wrapper.sh > REAL_SSH="$SSH" >+REAL_SSHD="$SSHD" > SSH="$SSHLOGWRAP" > > # Some test data. We make a copy because some tests will overwrite it. >diff --git a/servconf.c b/servconf.c >index 4fa896fd..ae31db41 100644 >--- a/servconf.c >+++ b/servconf.c >@@ -40,6 +40,11 @@ > #ifdef HAVE_UTIL_H > #include <util.h> > #endif >+#ifdef USE_SYSTEM_GLOB >+# include <glob.h> >+#else >+# include "openbsd-compat/glob.h" >+#endif > > #include "openbsd-compat/sys-queue.h" > #include "xmalloc.h" >@@ -74,6 +79,20 @@ static void add_one_listen_addr(ServerOptions *, const char *, > extern int use_privsep; > extern struct sshbuf *cfg; > >+#define INCLUDE_LIST_APPEND(includes, item) \ >+ do { \ >+ if ((includes)->count >= UINT16_MAX) \ >+ fatal("%s: Too many included files", __func__); \ >+ (item)->next = NULL; \ >+ if ((includes)->start == NULL) { \ >+ (includes)->start = (item); \ >+ } else if ((includes)->end != NULL) { \ >+ (includes)->end->next = (item); \ >+ } \ >+ (includes)->end = (item); \ >+ (includes)->count++; \ >+ } while (0) >+ > /* Initializes the server options to their default values. */ > > void >@@ -473,6 +492,14 @@ fill_default_server_options(ServerOptions *options) > > } > >+int process_server_config_line_depth(ServerOptions *options, char *line, >+ const char *filename, int linenum, int *activep, >+ struct connection_info *connectinfo, int inc_flags, int depth, >+ struct include_list *includes); >+void parse_server_config_depth(ServerOptions *options, const char *filename, >+ struct sshbuf *conf, struct include_list *includes, >+ struct connection_info *connectinfo, int flags, int *activep, int depth); >+ > /* Keyword tokens. */ > typedef enum { > sBadOption, /* == unknown option */ >@@ -502,7 +529,7 @@ typedef enum { > sAcceptEnv, sSetEnv, sPermitTunnel, > sMatch, sPermitOpen, sPermitListen, sForceCommand, sChrootDirectory, > sUsePrivilegeSeparation, sAllowAgentForwarding, >- sHostCertificate, >+ sHostCertificate, sInclude, > sRevokedKeys, sTrustedUserCAKeys, sAuthorizedPrincipalsFile, > sAuthorizedPrincipalsCommand, sAuthorizedPrincipalsCommandUser, > sKexAlgorithms, sCASignatureAlgorithms, sIPQoS, sVersionAddendum, >@@ -518,6 +545,8 @@ typedef enum { > #define SSHCFG_MATCH 0x02 /* allowed inside a Match section */ > #define SSHCFG_ALL (SSHCFG_GLOBAL|SSHCFG_MATCH) > >+#define SSHCFG_NEVERMATCH 0x04 /* Match/Host never matches; internal only */ >+ > /* Textual representation of the tokens. */ > static struct { > const char *name; >@@ -644,6 +673,7 @@ static struct { > { "trustedusercakeys", sTrustedUserCAKeys, SSHCFG_ALL }, > { "authorizedprincipalsfile", sAuthorizedPrincipalsFile, SSHCFG_ALL }, > { "kexalgorithms", sKexAlgorithms, SSHCFG_GLOBAL }, >+ { "include", sInclude, SSHCFG_ALL }, > { "ipqos", sIPQoS, SSHCFG_ALL }, > { "authorizedkeyscommand", sAuthorizedKeysCommand, SSHCFG_ALL }, > { "authorizedkeyscommanduser", sAuthorizedKeysCommandUser, SSHCFG_ALL }, >@@ -1217,10 +1247,20 @@ static const struct multistate multistate_tcpfwd[] = { > int > process_server_config_line(ServerOptions *options, char *line, > const char *filename, int linenum, int *activep, >- struct connection_info *connectinfo) >+ struct connection_info *connectinfo, struct include_list *includes) >+{ >+ return process_server_config_line_depth(options, line, filename, >+ linenum, activep, connectinfo, 0, 0, includes); >+} >+ >+int >+process_server_config_line_depth(ServerOptions *options, char *line, >+ const char *filename, int linenum, int *activep, >+ struct connection_info *connectinfo, int inc_flags, int depth, >+ struct include_list *includes) > { > char ch, *cp, ***chararrayptr, **charptr, *arg, *arg2, *p; >- int cmdline = 0, *intptr, value, value2, n, port; >+ int cmdline = 0, *intptr, value, value2, n, port, oactive; > SyslogFacility *log_facility_ptr; > LogLevel *log_level_ptr; > ServerOpCodes opcode; >@@ -1229,6 +1269,9 @@ process_server_config_line(ServerOptions *options, char *line, > long long val64; > const struct multistate *multistate_ptr; > const char *errstr; >+ struct include_item *item; >+ int found = 0; >+ glob_t gbuf; > > /* Strip trailing whitespace. Allow \f (form feed) at EOL only */ > if ((len = strlen(line)) == 0) >@@ -1255,7 +1298,7 @@ process_server_config_line(ServerOptions *options, char *line, > cmdline = 1; > activep = &cmdline; > } >- if (*activep && opcode != sMatch) >+ if (*activep && opcode != sMatch && opcode != sInclude) > debug3("%s:%d setting %s %s", filename, linenum, arg, cp); > if (*activep == 0 && !(flags & SSHCFG_MATCH)) { > if (connectinfo == NULL) { >@@ -1906,6 +1949,83 @@ process_server_config_line(ServerOptions *options, char *line, > *intptr = value; > break; > >+ case sInclude: >+ if (cmdline) >+ fatal("Include directive not supported as a command-line " >+ "option"); >+ >+ value = 0; >+ while ((arg = strdelim(&cp)) != NULL && *arg != '\0') { >+ value++; >+ found = 0; >+ if (*arg != '/' && *arg != '~') { >+ xasprintf(&arg2, "%s/%s", >+ SSHDIR, arg); >+ } else >+ arg2 = xstrdup(arg); >+ >+ /* >+ * don't let the Match in Included clobber >+ * the containing file's Match state. >+ */ >+ oactive = *activep; >+ /* browse cached list of files */ >+ for (item = includes->start; item != NULL; item = item->next) { >+ if (strcmp(item->selector, arg2) == 0) { >+ if (item->filename != NULL) >+ parse_server_config_depth(options, >+ item->filename, item->buffer, >+ includes, connectinfo, >+ (oactive ? 0 : SSHCFG_NEVERMATCH), >+ activep, depth + 1); >+ found = 1; >+ *activep = oactive; >+ } >+ } >+ if (found != 0) { >+ free(arg2); >+ continue; >+ } >+ >+ /* not in cache, a new glob */ >+ debug3("Glob configuration file to include %s", arg2); >+ if (glob(arg2, 0, NULL, &gbuf) == 0) { >+ for (n = 0; n < gbuf.gl_pathc; n++) { >+ debug3("Including configuration file %s", >+ gbuf.gl_pathv[n]); >+ item = malloc(sizeof(struct include_item)); >+ item->selector = strdup(arg2); >+ item->filename = strdup(gbuf.gl_pathv[n]); >+ item->buffer = sshbuf_new(); >+ load_server_config(item->filename, >+ item->buffer); >+ parse_server_config_depth(options, >+ item->filename, item->buffer, >+ includes, connectinfo, >+ (oactive ? 0 : SSHCFG_NEVERMATCH), >+ activep, depth + 1); >+ >+ /* append item to the end of the list */ >+ INCLUDE_LIST_APPEND(includes, item); >+ *activep = oactive; >+ } >+ } else { /* no match or other error */ >+ /* store placeholder to avoid aditional empty globs */ >+ item = malloc(sizeof(struct include_item)); >+ item->selector = strdup(arg2); >+ item->filename = NULL; >+ item->buffer = sshbuf_new(); >+ /* append item to the end of the list */ >+ INCLUDE_LIST_APPEND(includes, item); >+ } >+ globfree(&gbuf); >+ free(arg2); >+ } >+ if (value == 0) >+ fatal("%s line %d: missing argument - file to include", >+ filename, linenum); >+ break; >+ > case sMatch: > if (cmdline) > fatal("Match directive not supported as a command-line " >@@ -1914,7 +2034,7 @@ process_server_config_line(ServerOptions *options, char *line, > if (value < 0) > fatal("%s line %d: Bad Match condition", filename, > linenum); >- *activep = value; >+ *activep = (inc_flags & SSHCFG_NEVERMATCH) ? 0 : value; > break; > > case sPermitListen: >@@ -2231,12 +2351,13 @@ load_server_config(const char *filename, struct sshbuf *conf) > > void > parse_server_match_config(ServerOptions *options, >- struct connection_info *connectinfo) >+ struct include_list *includes, struct connection_info *connectinfo) > { > ServerOptions mo; > > initialize_server_options(&mo); >- parse_server_config(&mo, "reprocess config", cfg, connectinfo); >+ parse_server_config(&mo, "reprocess config", cfg, includes, >+ connectinfo); > copy_set_server_options(options, &mo, 0); > } > >@@ -2381,20 +2502,35 @@ copy_set_server_options(ServerOptions *dst, ServerOptions *src, int preauth) > > void > parse_server_config(ServerOptions *options, const char *filename, >- struct sshbuf *conf, struct connection_info *connectinfo) >+ struct sshbuf *conf, struct include_list *includes, >+ struct connection_info *connectinfo) >+{ >+ int active = connectinfo ? 0 : 1; >+ parse_server_config_depth(options, filename, conf, includes, >+ connectinfo, 0, &active, 0); >+} >+ >+#define SERVCONF_MAX_DEPTH 16 >+ >+void >+parse_server_config_depth(ServerOptions *options, const char *filename, >+ struct sshbuf *conf, struct include_list *includes, >+ struct connection_info *connectinfo, int flags, int *activep, int depth) > { >- int active, linenum, bad_options = 0; >+ int linenum, bad_options = 0; > char *cp, *obuf, *cbuf; > >+ if (depth < 0 || depth > SERVCONF_MAX_DEPTH) >+ fatal("Too many recursive configuration includes"); >+ > debug2("%s: config %s len %zu", __func__, filename, sshbuf_len(conf)); > > if ((obuf = cbuf = sshbuf_dup_string(conf)) == NULL) > fatal("%s: sshbuf_dup_string failed", __func__); >- active = connectinfo ? 0 : 1; > linenum = 1; > while ((cp = strsep(&cbuf, "\n")) != NULL) { >- if (process_server_config_line(options, cp, filename, >- linenum++, &active, connectinfo) != 0) >+ if (process_server_config_line_depth(options, cp, filename, >+ linenum++, activep, connectinfo, flags, depth, includes) != 0) > bad_options++; > } > free(obuf); >diff --git a/servconf.h b/servconf.h >index 54e0a8d8..c5216878 100644 >--- a/servconf.h >+++ b/servconf.h >@@ -223,6 +223,19 @@ struct connection_info { > const char *rdomain; /* routing domain if available */ > }; > >+/* List of included files for re-exec from the parsed configuration */ >+struct include_item { >+ char *selector; >+ char *filename; >+ struct sshbuf *buffer; >+ struct include_item *next; >+}; >+struct include_list { >+ u_int16_t count; >+ struct include_item *start; >+ struct include_item *end; >+}; >+ > > /* > * These are string config options that must be copied between the >@@ -262,12 +275,13 @@ struct connection_info *get_connection_info(struct ssh *, int, int); > void initialize_server_options(ServerOptions *); > void fill_default_server_options(ServerOptions *); > int process_server_config_line(ServerOptions *, char *, const char *, int, >- int *, struct connection_info *); >+ int *, struct connection_info *, struct include_list *includes); > void process_permitopen(struct ssh *ssh, ServerOptions *options); > void load_server_config(const char *, struct sshbuf *); > void parse_server_config(ServerOptions *, const char *, struct sshbuf *, >- struct connection_info *); >-void parse_server_match_config(ServerOptions *, struct connection_info *); >+ struct include_list *includes, struct connection_info *); >+void parse_server_match_config(ServerOptions *, >+ struct include_list *includes, struct connection_info *); > int parse_server_match_testspec(struct connection_info *, char *); > int server_match_spec_complete(struct connection_info *); > void copy_set_server_options(ServerOptions *, ServerOptions *, int); >diff --git a/sshd.c b/sshd.c >index cbd3bce9..8c09f0a1 100644 >--- a/sshd.c >+++ b/sshd.c >@@ -250,6 +250,13 @@ struct sshauthopt *auth_opts = NULL; > /* sshd_config buffer */ > struct sshbuf *cfg; > >+/* Included files from the configuration file */ >+struct include_list includes = { >+ .count = 0, >+ .start = NULL, >+ .end = NULL, >+}; >+ > /* message to be displayed after login */ > struct sshbuf *loginmsg; > >@@ -852,7 +859,8 @@ usage(void) > static void > send_rexec_state(int fd, struct sshbuf *conf) > { >- struct sshbuf *m; >+ struct sshbuf *m = NULL; >+ struct include_item *item = NULL; > int r; > > debug3("%s: entering fd = %d config len %zu", __func__, fd, >@@ -867,6 +875,14 @@ send_rexec_state(int fd, struct sshbuf *conf) > fatal("%s: sshbuf_new failed", __func__); > if ((r = sshbuf_put_stringb(m, conf)) != 0) > fatal("%s: buffer error: %s", __func__, ssh_err(r)); >+ if ((r = sshbuf_put_u16(m, includes.count)) != 0) >+ fatal("%s: buffer error: %s", __func__, ssh_err(r)); >+ for (item = includes.start; item != NULL; item = item->next) { >+ if ((r = sshbuf_put_cstring(m, item->selector)) != 0 || >+ (r = sshbuf_put_cstring(m, item->filename)) != 0 || >+ (r = sshbuf_put_stringb(m, item->buffer)) != 0) >+ fatal("%s: buffer error: %s", __func__, ssh_err(r)); >+ } > > #if defined(WITH_OPENSSL) && !defined(OPENSSL_PRNG_ONLY) > rexec_send_rng_seed(m); >@@ -886,7 +902,7 @@ recv_rexec_state(int fd, struct sshbuf *conf) > struct sshbuf *m; > u_char *cp, ver; > size_t len; >- int r; >+ int r, i; > > debug3("%s: entering fd = %d", __func__, fd); > >@@ -905,6 +921,25 @@ recv_rexec_state(int fd, struct sshbuf *conf) > #if defined(WITH_OPENSSL) && !defined(OPENSSL_PRNG_ONLY) > rexec_recv_rng_seed(m); > #endif >+ if ((r = sshbuf_get_u16(m, &includes.count)) != 0) >+ fatal("%s: buffer error: %s", __func__, ssh_err(r)); >+ for (i = 0; i < includes.count; i++) { >+ struct include_item *item = calloc(1, sizeof(struct include_item)); >+ >+ if (item == NULL) >+ fatal("%s: Failed to allocate memory.", __func__); >+ if ((item->buffer = sshbuf_new()) == NULL) >+ fatal("%s: sshbuf_new failed", __func__); >+ if ((r = sshbuf_get_cstring(m, &item->selector, NULL)) != 0 || >+ (r = sshbuf_get_cstring(m, &item->filename, NULL)) != 0 || >+ (r = sshbuf_get_stringb(m, item->buffer)) != 0) >+ fatal("%s: buffer error: %s", __func__, ssh_err(r)); >+ if (includes.start == NULL) >+ includes.start = item; >+ if (includes.end != NULL) >+ includes.end->next = item; >+ includes.end = item; >+ } > > free(cp); > sshbuf_free(m); >@@ -1564,7 +1599,7 @@ main(int ac, char **av) > case 'o': > line = xstrdup(optarg); > if (process_server_config_line(&options, line, >- "command-line", 0, NULL, NULL) != 0) >+ "command-line", 0, NULL, NULL, &includes) != 0) > exit(1); > free(line); > break; >@@ -1633,7 +1668,7 @@ main(int ac, char **av) > load_server_config(config_file_name, cfg); > > parse_server_config(&options, rexeced_flag ? "rexec" : config_file_name, >- cfg, NULL); >+ cfg, &includes, NULL); > > /* Fill in default values for those options not explicitly set. */ > fill_default_server_options(&options); >@@ -1843,7 +1878,7 @@ main(int ac, char **av) > */ > if (connection_info == NULL) > connection_info = get_connection_info(ssh, 0, 0); >- parse_server_match_config(&options, connection_info); >+ parse_server_match_config(&options, &includes, connection_info); > dump_config(&options); > } > >diff --git a/sshd_config.5 b/sshd_config.5 >index 142f84a1..8afc78b8 100644 >--- a/sshd_config.5 >+++ b/sshd_config.5 >@@ -794,7 +794,19 @@ during > and use only the system-wide known hosts file > .Pa /etc/ssh/known_hosts . > The default is >-.Cm no . >+.Dq no . >+.It Cm Include >+Include the specified configuration file(s). >+Multiple path names may be specified and each pathname may contain >+.Xr glob 7 >+wildcards. >+Files without absolute paths are assumed to be in >+.Pa /etc/ssh . >+.Cm Include >+directive may appear inside a >+.Cm Match >+block >+to perform conditional inclusion. > .It Cm IPQoS > Specifies the IPv4 type-of-service or DSCP class for the connection. > Accepted values are
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
Attachments on
bug 2468
:
2706
|
2869
|
3223
|
3250
|
3333
|
3350
|
3351